Repository: coreybutler/nvm-windows Branch: master Commit: 5b18223ca19f Files: 47 Total size: 250.4 KB Directory structure: gitextract_df78uor0/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feaure_request.yml │ ├── OLD_ISSUE_TEMPLATE │ ├── dependabot.yml │ └── workflows/ │ ├── autotag.yml │ ├── bot.yml │ ├── release.yml │ ├── scanner.yml │ └── winget.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SUPPORT.md ├── assets/ │ ├── buildtools/ │ │ ├── Default.isl │ │ ├── ISPPBuiltins.iss │ │ ├── Setup.e32 │ │ └── SetupLdr.e32 │ ├── elevate.cmd │ ├── elevate.vbs │ ├── install.cmd │ ├── run.cmd │ ├── setuserenv.vbs │ └── unsetuserenv.vbs ├── build.js ├── examples/ │ └── settings.txt ├── nvm.iss └── src/ ├── arch/ │ └── arch.go ├── author/ │ └── bridge.go ├── encoding/ │ └── encoding.go ├── file/ │ └── file.go ├── go.mod ├── go.sum ├── manifest.json ├── node/ │ └── node.go ├── nvm.go ├── semver/ │ └── semver.go ├── upgrade/ │ ├── check.go │ ├── notification.go │ ├── register.go │ └── upgrade.go ├── utility/ │ ├── logging.go │ └── rename.go └── web/ └── web.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text eol=lf *.exe binary *.dll binary *.e32 binary ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [coreybutler] patreon: # coreybutler open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with a single custom sponsorship URL ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: "🕷️ Bug Report" description: Report errors or unexpected behavior title: "[Issue]: " labels: - "needs review" body: - type: markdown attributes: value: | ## Before submitting your issue, please do the following: 1. Make sure you're running Windows. If you're using macOS, Linux, WSL/WSL2, or a Docker container using Linux, then you probably want [nvm-sh](https://github.com/nvm-sh/nvm) (this is NVM _for Windows_). 1. **Run `nvm debug` (v1.1.11+) to see if you can resolve your issue yourself.** 1. Review the **[common issues and resolutions](https://github.com/coreybutler/nvm-windows/wiki/Common-Issues)** wiki page. 1. **Search existing issues to see if your issue has already been opened/closed (help prevent duplicates).** 1. Remember, there is a [wiki](https://github.com/coreybutler/nvm-windows/wiki) that answers questions about installation types, building from source, and project philosophy concerns. 1. **[Read the README](https://github.com/coreybutler/nvm-windows)**. We took the time to write it for you. Many people ignore this. 1. If your issue is actually a question, please use the [discussions page](https://github.com/coreybutler/nvm-windows/discussions) so the whole community can help. 1. If you're seeking commercial help or requesting something you really need _for work/employment_, please consider [sponsoring](https://github.com/sponsors/coreybutler) the change/fix. - type: textarea id: issue attributes: label: What happened? description: Please provide a clear and concise description of what happened. placeholder: Tell us what you see! validations: required: true - type: textarea id: expected attributes: label: What did you expect to happen? description: Please provide a clear and concise description of what you expected to happen. placeholder: Tell us what you thought you'd see! validations: required: true - type: dropdown id: version attributes: label: Version description: What version are you running? Run `nvm version` to find out. **Please don't assume. Check the version.** options: - 1.2.0 or newer (Default) - 1.1.12 - 1.1.11 - 1.1.10 - 1.1.9 - 1.1.8 - 1.1.7 - 1.1.6 or older validations: required: true - type: dropdown id: os-version attributes: label: Which version of Windows? multiple: true options: - Windows 11+ (Default) - Windows 10 - Windows 8.1 - Windows 8 - Windows 7 - Windows Server 2022 - Windows Server 2019 - Older - type: dropdown id: os-language attributes: label: Which locale? description: Are you running a non-English version of Windows? multiple: false options: - English (Default) - Western European - Asian - Other Non-English - type: dropdown attributes: label: Which shell are you running NVM4W in? options: - "Command Prompt" - PowerShell - WSL or WSL2 - Cygwin - Mingw64 - ConEmu - Commander - Hyper - Other multiple: true - type: dropdown id: permissions attributes: label: User Permissions? options: - Administrative Privileges, Elevated - Administrative Privileges, Non-Elevated - Standard User, Elevated - Standard Use, Non-Elevated - I don't know (Default) - Other, please describe validations: required: true - type: dropdown id: developer-mode attributes: label: Is Developer Mode enabled? description: See [Enable your device for development](https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development). options: - "Yes" - No (Default) - type: textarea id: logs attributes: label: Relevant log/console output description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: Shell validations: required: false - type: textarea attributes: label: Debug Output description: | If possible, please provide the output of `nvm debug`. The output of `nvm debug` looks like this ``` is not using admin or elevated rights. PowerShell Windows Developer Mode: UNKNOWN (user cannot read registry) NVM4W Version: 1.2.0 NVM4W Path: C:\Users\username\AppData\Roaming\nvm\nvm.exe NVM4W Settings: C:\Users\username\AppData\Roaming\nvm\settings.txt NVM_HOME: C:\Users\uername\AppData\Roaming\nvm NVM_SYMLINK: C:\Program Files\nodejs Node Installations: C:\Users\username\AppData\Roaming\nvm Active Node.js Version: v20.7.0 No problems detected. Find help at https://github.com/coreybutler/nvm-windows/wiki/Common-Issues ``` If you only see the standard help menu (as shown below), then you are probably running v1.1.10 or below. Upgrade to NVM4W 1.1.11+. ```sh Running version 1.1.12. Usage: nvm arch : Show if node is running in 32 or 64 bit mode. nvm current : Display active version. nvm debug : Check the NVM4W process for known problems (troubleshooter). nvm install [arch] : The version can be a specific version, "latest" for the latest current version, or "lts" for the most recent LTS version. Optionally specify whether to install the 32 or 64 bit version (defaults to system arch). Set [arch] to "all" to install 32 AND 64 bit versions. Add --insecure to the end of this command to bypass SSL validation of the remote download server. nvm list [available] : List the node.js installations. Type "available" at the end to see what can be installed. Aliased as ls. nvm on : Enable node.js version management. nvm off : Disable node.js version management. ``` render: Shell validations: required: true - type: textarea id: misc attributes: label: Anything else? description: | Links? References? Anything that will give us more context about the issue you are encountering! Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: "\u003F General Question" url: https://github.com/coreybutler/nvm-windows/discussions about: Discuss features or general questions ================================================ FILE: .github/ISSUE_TEMPLATE/feaure_request.yml ================================================ name: "⭐ Feature or enhancement request" title: "[Feature]: " description: Propose something new. labels: - "needs review" - "enhancement request" body: - type: markdown attributes: value: | **Be aware that NVM for Windows feature development is currently [frozen](https://github.com/coreybutler/nvm-windows/wiki/Runtime#nvm4w-feature-freeze).** We are actively developing [Runtime](https://github.com/coreybutler/nvm-windows/wiki/Runtime), a new JavaScript runtime manager to succeed NVM4W. Feature requests submitted here will be considered for Runtime. For more information, see the [new features explanation](https://github.com/coreybutler/nvm-windows/wiki/New-Features). If you are requesting an nvm-sh feature, please recognize that it is a different project from NVM4W (nvm-windows). For more about feature parity, see [Why Aren't Some nvm for macOS/Linux Features Supported?](https://github.com/coreybutler/nvm-windows/wiki/Common-Issues#why-isnt-nvmrc-supported-why-arent-some-nvm-for-macoslinux-features-supported) - type: textarea attributes: label: Description of the new feature / enhancement placeholder: What is the expected behavior of the proposed feature? validations: required: true - type: textarea attributes: label: Scenario when this would be used? placeholder: What is the scenario this would be used? Why is this important to your workflow as a power user? validations: required: true - type: textarea attributes: label: Supporting information placeholder: "Having additional evidence, data, tweets, blog posts, research, ... anything is extremely helpful. This information provides context to the scenario that may otherwise be lost." validations: required: false - type: markdown attributes: value: Please limit one request per issue. ================================================ FILE: .github/OLD_ISSUE_TEMPLATE ================================================ **NVM4W version:** ### My Environment ### Expected Behavior ### Actual Behavior ### Steps to reproduce the problem: ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/autotag.yml ================================================ name: Autotag on: push: branches: - main - master jobs: autotag: runs-on: ubuntu-latest outputs: tag_name: ${{ steps.create_tag.outputs.tag_name}} steps: - name: Checkout code uses: actions/checkout@v4 - name: Identify version id: read_version run: | cd ./src echo identifying version... VERSION=$(jq -r '.version' manifest.json) echo "version=$VERSION" >> $GITHUB_OUTPUT cd ../ - name: Check if tag exists id: check_tag run: | VERSION=${{ steps.read_version.outputs.version }} if git rev-parse "$VERSION" >/dev/null 2>&1; then echo "Tag $VERSION already exists." echo "tag_exists=true" >> $GITHUB_OUTPUT else echo "Tag $VERSION does not exist." echo "tag_exists=false" >> $GITHUB_OUTPUT fi - name: Import GPG Key uses: crazy-max/ghaction-import-gpg@v6.2.0 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} git_tag_gpgsign: true git_user_signingkey: true - name: Create tag if: success() && steps.check_tag.outputs.tag_exists == 'false' id: create_tag run: | VERSION=${{ steps.read_version.outputs.version }} git tag -s "$VERSION" -m "Release $VERSION" git push origin "$VERSION" echo "$VERSION tag created." ================================================ FILE: .github/workflows/bot.yml ================================================ name: 'Close stale issues and PR' on: schedule: - cron: '30 1 * * *' jobs: stale: runs-on: ubuntu-latest steps: - 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.' close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' days-before-stale: 30 days-before-close: 7 days-before-pr-close: -1 start-date: '2021-09-15T05:00:00Z' exempt-all-assignees: true exempt-all-milestones: true exempt-issue-labels: 'bug,not stale,help wanted,consider for rt' ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: workflow_run: workflows: ["Autotag"] types: - completed jobs: build: runs-on: windows-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} permissions: id-token: write contents: write attestations: write env: GO_VERSION: 1.23 QUIKGO_VERSION: 1.2.6 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} AUTHOR_BRIDGE_TOKEN: ${{ secrets.AUTHOR_BRIDGE_TOKEN }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Extract Tag from Event id: extract_tag shell: pwsh run: | Write-Host "Event payload:" Get-Content $env:GITHUB_EVENT_PATH # Extract the commit SHA from the event payload $COMMIT_SHA = (Get-Content -Raw $env:GITHUB_EVENT_PATH | ConvertFrom-Json).workflow_run.head_commit.id if (-not $COMMIT_SHA) { Write-Host "Error: No commit SHA found in the event payload." exit 1 } Write-Host "Commit SHA: $COMMIT_SHA" # Fetch tags and find the tag associated with the commit git fetch --tags $TAG_REF = git tag --contains $COMMIT_SHA | ForEach-Object { $_.Trim() } | Select-Object -First 1 if (-not $TAG_REF) { Write-Host "Error: No tag found for this commit." exit 1 } Write-Host "Extracted tag reference: $TAG_REF" echo "TAG=$TAG_REF" | Out-File -Append -FilePath $env:GITHUB_ENV - name: Display Tag shell: pwsh run: | Write-Host "Identified Tag: $env:TAG" - name: Set up Go uses: actions/setup-go@v5 with: go-version: '${{ env.GO_VERSION }}' - name: Cache Go Modules uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Cache QuikGo uses: actions/cache@v4 with: path: | ~/go/bin key: ${{ runner.os }}-qgo-v${{ env.QUIKGO_VERSION }} restore-keys: | ${{ runner.os }}-qgo- - name: Install QuikGo run: | if (-not (go list -m github.com/quikdev/go/cmd/qgo@v${{ env.QUIKGO_VERSION }})) { if (-not (go list -m github.com/quikdev/go/cmd/qgo@v${{ env.QUIKGO_VERSION }})) { go install github.com/quikdev/go/cmd/qgo@v${{ env.QUIKGO_VERSION }} } else { echo "QuikGo version ${{ env.QUIKGO_VERSION }} already installed." } } else { echo "QuikGo version ${{ env.QUIKGO_VERSION }} already installed." } - name: Confirm QuikGo Installation run: qgo --version - name: Build NVM for Windows run: | cd src qgo build --profile=release echo ${{ github.workspace }}\bin dir ${{ github.workspace }}\bin cd ..\ # - name: Compress Executables # uses: crazy-max/ghaction-upx@v3 # with: # version: latest # files: | # ${{ github.workspace }}/bin/*.exe # args: --brute - name: Download Resource Hacker run: | Invoke-WebRequest -Uri "https://www.angusj.com/resourcehacker/resource_hacker.zip" -OutFile "resource_hacker.zip" Expand-Archive -Path "resource_hacker.zip" -DestinationPath "$env:USERPROFILE\resource_hacker" - name: Apply icon to executable run: | $resourceHackerPath = "$env:USERPROFILE\resource_hacker\ResourceHacker.exe" $exe = "${{ github.workspace }}\bin\nvm.exe" $iconFile = "${{ github.workspace }}\assets\nvm.ico" & $resourceHackerPath -open $exe -save $exe -action add -res $iconFile -mask ICONGROUP,MAINICON - name: Sign Executables uses: azure/trusted-signing-action@v0.5.0 with: azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} endpoint: ${{ secrets.AZURE_ENDPOINT }} trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} files-folder: ${{ github.workspace }}\bin files-folder-filter: exe,dll file-digest: SHA256 timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 - name: Identify & Download Latest Author-NVM Bridge App (Using GitHub API) id: get_release run: | $repoOwner = "author" $repoName = "author-nvm" $token = $env:AUTHOR_BRIDGE_TOKEN # Ensure the token is provided as a secret # Fetch the latest release information Write-Host "GET https://api.github.com/repos/$repoOwner/$repoName/releases" $response = Invoke-RestMethod -Uri "https://api.github.com/repos/$repoOwner/$repoName/releases" ` -Headers @{ Authorization = "token $token"; Accept = "application/vnd.github.v3+json"; } if ($response -and $response[0].tag_name) { $releaseTag = $response[0].tag_name Write-Host "Latest release tag identified: $releaseTag" # Fetch the release details for the identified tag Write-Host "GET https://api.github.com/repos/$repoOwner/$repoName/releases/tags/$releaseTag" $releaseDetails = Invoke-RestMethod -Uri "https://api.github.com/repos/$repoOwner/$repoName/releases/tags/$releaseTag" ` -Headers @{ Authorization = "token $token"; Accept = "application/vnd.github.v3+json"; } # Extract asset ID for author-nvm.exe $asset = $releaseDetails.assets | Where-Object { $_.name -eq 'author-nvm.exe' } | Select-Object -First 1 if ($asset -and $asset.id) { $assetId = $asset.id Write-Host "Asset ID for author-nvm.exe: $assetId" "ASSET_ID=$assetId" | Out-File -Append -FilePath $env:GITHUB_ENV # Download asset $assetDownloadUrl = "https://api.github.com/repos/$repoOwner/$repoName/releases/assets/$assetId" Write-Host "GET $assetDownloadUrl" Invoke-WebRequest -Uri $assetDownloadUrl ` -Headers @{ Authorization = "token $token"; Accept = "application/octet-stream"; } ` -OutFile "$env:GITHUB_WORKSPACE\bin\author-nvm.exe" } else { Write-Error "author-nvm.exe not found in the latest release." exit 1 } } else { Write-Error "No release tags found in the repository." exit 1 } - name: Generate Core Assets run: | $bin = "${{ github.workspace }}\bin" $license = "${{ github.workspace }}\LICENSE" $source = "${{ github.workspace }}\assets" $target = "${{ github.workspace }}\.tmp" $distros = "${{ github.workspace }}\.dist" if (!(Test-Path -Path $target)) { New-Item -ItemType Directory -Path $target } # Copy binaries to distribution Get-ChildItem -Path $bin -File | ForEach-Object { Copy-Item -Path $_.FullName -Destination $target } # Copy assets to distribution Get-ChildItem -Path $source -File | ForEach-Object { Copy-Item -Path $_.FullName -Destination $target } # Copy license to distribution Copy-Item -Path $license -Destination $target shell: pwsh - name: Generate Asset Distribution run: | $source = "${{ github.workspace }}\.tmp" $manual = "${{ github.workspace }}\.dist\nvm-noinstall.zip" $checksum = "${{ github.workspace }}\.dist\nvm-noinstall.zip.checksum.txt" if (Test-Path -Path $manual) { Remove-Item -Path $manual -Force } & "C:\Program Files\7-Zip\7z.exe" a -tzip $manual "$source\*" -mx=9 $hash = Get-FileHash -Path $manual -Algorithm MD5 $hash.Hash | Out-File -FilePath $checksum -Encoding utf8 shell: pwsh # - name: Install Inno Setup # run: | # choco install innosetup -y # # Verify installation # if (!(Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe")) { # Write-Error "Inno Setup installation failed." # exit 1 # } - name: Generate Installer run: | $iss = "${{ github.workspace }}\nvm.iss" $distros = "${{ github.workspace }}\.dist" if (!(Test-Path -Path $iss)) { Write-Error "Inno Setup Script file not found: $iss" exit 1 } # Replace version placeholder in the .iss file $version = $env:TAG -replace "^v", "" (Get-Content -Path $iss) -replace "{{VERSION}}", $version | Set-Content -Path $iss # Output the file to assure replacements worked Get-Content $iss # Get the files and directories in the source directory $items = Get-ChildItem -Path $source -Recurse $source = "${{ github.workspace }}\.tmp" $destination = "${{ github.workspace }}\bin" foreach ($item in $items) { # Construct the target path $target = $item.FullName -replace [regex]::Escape($source), [regex]::Escape($destination) if ($item.PSIsContainer) { # Create directories if they don't exist if (-not (Test-Path -Path $target)) { New-Item -ItemType Directory -Path $target } } else { # Copy files if they don't already exist if (-not (Test-Path -Path $target)) { Copy-Item -Path $item.FullName -Destination $target } } } Write-Host "Available assets in bin directory:" dir ${{ github.workspace }}\bin Write-Host "Available assets in .tmp directory:" dir ${{ github.workspace }}\.tmp Write-Host "Available build tools:" dir ${{ github.workspace }}\assets\buildtools ${{ github.workspace }}\assets\buildtools\iscc.exe "$iss" "/o$distros" # iscc "$iss" "/o$distros" shell: pwsh - name: Sign Installer uses: azure/trusted-signing-action@v0.5.0 with: azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-client-id: ${{ secrets.INSTALLER_AZURE_CLIENT_ID }} azure-client-secret: ${{ secrets.INSTALLER_AZURE_CLIENT_SECRET }} endpoint: ${{ secrets.AZURE_ENDPOINT }} trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} files-folder: ${{ github.workspace }}\.dist files-folder-filter: exe,dll file-digest: SHA256 timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 - name: Generate Official Distribution run: | $source = "${{ github.workspace }}\.dist\nvm-setup.exe" $distro = "${{ github.workspace }}\.dist\nvm-setup.zip" $checksum = "${{ github.workspace }}\.dist\nvm-setup.zip.checksum.txt" if (!(Test-Path -Path $source)) { Write-Error "Source file for ZIP not found: $source" exit 1 } & "C:\Program Files\7-Zip\7z.exe" a -tzip $distro "$source" -mx=9 if (!(Test-Path -Path $distro)) { Write-Error "ZIP file generation failed." exit 1 } $hash = Get-FileHash -Path $distro -Algorithm MD5 $hash.Hash | Out-File -FilePath $checksum -Encoding utf8 shell: pwsh - name: Attestation uses: actions/attest-build-provenance@v2 id: attest with: subject-path: | ${{ github.workspace }}/bin/*.exe ${{ github.workspace }}/.dist/*.exe ${{ github.workspace }}/.dist/*.zip ${{ github.workspace }}/.dist/*.checksum.txt github-token: ${{ secrets.GITHUB_TOKEN }} - name: Release uses: softprops/action-gh-release@v2 if: success() with: tag_name: ${{ env.TAG }} files: | ${{ github.workspace }}/.dist/* fail_on_unmatched_files: true generate_release_notes: true prerelease: false draft: false make_latest: true - name: Add Attestation Report to Release Notes if: success() uses: softprops/action-gh-release@v2 with: tag_name: ${{ env.TAG }} body: |
:office: **[Attestation Report](${{ steps.attest.outputs.attestation-url }})** append_body: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} rollback: runs-on: ubuntu-latest needs: build if: failure() steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 # Ensures that all references, including tags, are fetched ref: ${{ github.event.workflow_run.head_sha }} - name: Show current commit run: | git log -1 --decorate git tag --contains HEAD - name: Get the commit associated with the trigger id: get_commit run: | if [ -f "$GITHUB_EVENT_PATH" ]; then # Try to get the head SHA from workflow_run COMMIT_SHA=$(jq -r '.head_commit.id' < "$GITHUB_EVENT_PATH") if [ "$COMMIT_SHA" = "null" ]; then COMMIT_SHA=$(jq -r '.workflow_run.head_commit.id' < "$GITHUB_EVENT_PATH") fi if [ -z "$COMMIT_SHA" ] || [ "$COMMIT_SHA" = "null" ]; then echo "Error: Unable to retrieve commit SHA from GITHUB_EVENT_PATH" cat "$GITHUB_EVENT_PATH" echo "HEAD: $(git rev-parse HEAD)" exit 1 fi echo "Commit SHA: $COMMIT_SHA" echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV else echo "Error: GITHUB_EVENT_PATH not found" exit 1 fi - name: Fetch the tag associated with the commit using git id: fetch_tag run: | # Debugging: print all tags in the repository git tag # Get the tag associated with the commit echo "Finding tag associated with the commit..." TAG=$(git tag --contains "$COMMIT_SHA" | head -n 1) echo "Tag associated with the commit is: $TAG" # Debugging: Check if the tag is found if [ -z "$TAG" ]; then echo "No tag found for this commit." else echo "Found tag: $TAG" fi echo "TAG=$TAG" >> $GITHUB_ENV - name: Delete release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Use the TAG from the environment variable if [ -z "$TAG" ]; then echo "No tag to delete." exit 1 fi RELEASE_ID=$(gh release view "$TAG" --json id --jq '.id' || echo "") if [ -n "$RELEASE_ID" ]; then gh release delete "$TAG" --yes echo "Release $TAG deleted successfully." else echo "Release $TAG not found." fi - name: Delete tag associated with release run: | # Use the TAG from the environment variable if [ -z "$TAG" ]; then echo "No tag to delete." exit 1 fi git tag -d "$TAG" git push --delete origin "$TAG" - name: Notify Rollback run: echo "Rollback completed for release $TAG" ================================================ FILE: .github/workflows/scanner.yml ================================================ name: Virus Scan on: workflow_run: workflows: ["Release"] types: - completed jobs: scan: if: ${{ github.event.workflow_run.conclusion }} == 'success' runs-on: ubuntu-latest steps: - name: Get latest release details id: get_latest_release uses: actions/github-script@v7 with: script: | const { data: latestRelease } = await github.rest.repos.getLatestRelease({ owner: context.repo.owner, repo: context.repo.repo }); core.setOutput('tag', latestRelease.tag_name); core.setOutput('assets', JSON.stringify(latestRelease.assets)); - name: Download release assets env: ASSETS_JSON: ${{ steps.get_latest_release.outputs.assets }} run: | echo "Assets: $ASSETS_JSON" for asset in $(echo $ASSETS_JSON | jq -r '.[].browser_download_url'); do echo "Downloading $asset" curl -L -O "$asset" done - name: Extract Main Executables run: | unzip nvm-noinstall.zip -d ./ - name: List downloaded assets run: ls -alh ./ - name: Scan with VirusTotal uses: WoozyMasta/virustotal-action@v1.0.0 id: scan with: vt_api_key: ${{ secrets.VIRUSTOTAL_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} file_globs: | nvm.exe author-nvm.exe nvm-setup.exe - name: Generate release notes id: release_notes run: | input_list="${{ steps.scan.outputs.results }}" notes=":bulb: **Antivirus Report**
" # Output the result echo "Generated Notes:" echo "$notes" echo "NOTES=$notes" >> $GITHUB_OUTPUT - name: Update release body uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.get_latest_release.outputs.tag }} body: | ${{ steps.release_notes.outputs.NOTES }} append_body: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # - name: Scan with VirusTotal # uses: crazy-max/ghaction-virustotal@v4 # with: # vt_api_key: ${{ secrets.VIRUSTOTAL_API_KEY }} # files: | # *.exe # .exe$ # update_release_body: true # github_token: ${{ secrets.GITHUB_TOKEN }} # request_rate: 4 ================================================ FILE: .github/workflows/winget.yml ================================================ name: Publish to Winget on: release: types: [released] jobs: publish: runs-on: ubuntu-latest steps: - uses: vedantmgoyal2009/winget-releaser@v2 with: identifier: CoreyButler.NVMforWindows version: ${{ github.event.release.tag_name }} installers-regex: 'nvm-setup\.exe$' token: ${{ secrets.WINGET_TOKEN }} ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so *.old # Folders _obj _test bin # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe !utilities/**/*.exe *.test *.prof **/.* dist src/v* bin/*.exe bin/*.log bin/LICENSE !bin/buildtools/* !assets/buildtools/* bin/*.zip bin/nvm/* !.gitignore !.gitattributes !.github tests ================================================ FILE: CONTRIBUTING.md ================================================ ## Before you submit an issue.... There have been a lot of duplicate issues filed. Please do a simple search for your issue before posting something new. If you just have a question and no real problem, please use the [StackOverflow tag](https://stackoverflow.com/questions/tagged/nvm-windows) the [Gitter channel](https://gitter.im/coreybutler/nvm-windows) instead of clogging up the bug tracker with questions. ## Common Issues & Resolutions See the [Common Issues](https://github.com/coreybutler/nvm-windows/wiki/Common-Issues) wiki page before submitting. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2025 Author Software Inc. Copyright (c) 2022 Ecor Ventures LLC. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

We are working full time on Author, including Runtime, the successor to NVM for Windows.

Complete this form to provide your thoughts and sign up for progress updates.

Updates will also be posted on the Author Software LinkedIn Page.


NVM for Windows

The Microsoft/npm/Google recommended Node.js version manager for Windows.
This is not the same thing as nvm! (expand for details) _The original [nvm](https://github.com/nvm-sh/nvm) is a completely separate project for Mac/Linux only._ This project uses an entirely different philosophy and is not just a clone of nvm. Details are listed in [Why another version manager?](#bulb-why-another-version-manager) and [what's the big difference?](#bulb-whats-the-big-difference).
[![Download Latest Now](https://img.shields.io/badge/-Download%20Now!-%2322A6F2)](https://github.com/coreybutler/nvm-windows/releases) [![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/coreybutler/nvm-windows?label=Latest%20Release&style=social&x=1)]((https://github.com/coreybutler/nvm-windows/releases)) ![GitHub Release Date](https://img.shields.io/github/release-date/coreybutler/nvm-windows?label=Released&style=social) ![GitHub all releases](https://img.shields.io/github/downloads/coreybutler/nvm-windows/total?label=Downloads&style=social) [![Discuss](https://img.shields.io/badge/-Discuss-blue)](https://github.com/coreybutler/nvm-windows/discussions) [![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fhashtags%3Dnodejs%26original_referer%3Dhttp%253A%252F%252F127.0.0.1%253A91%252F%26text%3DCheck%2520out%2520NVM%2520for%2520Windows%21%26tw_p%3Dtweetbutton%26url%3Dhttp%253A%252F%252Fgithub.com%252Fcoreybutler%252Fnvm-windows%26via%3Dgoldglovecb)](https://twitter.com/intent/tweet?hashtags=nodejs&original_referer=http%3A%2F%2F127.0.0.1%3A91%2F&text=Check%20out%20NVM%20for%20Windows!&tw_p=tweetbutton&url=http%3A%2F%2Fgithub.com%2Fcoreybutler%2Fnvm-windows&via=goldglovecb)
[![Download Stable](https://img.shields.io/badge/-Download%20Stable-%2322A6F2)](https://github.com/coreybutler/nvm-windows/releases/tag/1.1.12) [![GitHub tag (stable SemVer)](https://img.shields.io/badge/Stable%20Version-v1.1.12-1?style=social)]((https://github.com/coreybutler/nvm-windows/releases/tag/1.1.12/))
Latest vs Stable? The latest version (v1.2.x) is a [transition version]([https://](https://opensource.author.io/nvm-for-windows-v120)) as we complete work on [Runtime](https://github.com/coreybutler/nvm-windows/wiki/Runtime), the successor to NVM for Windows. However; there is a known bug that may impact some users when installing old (EOL) versions of Node.js. The last stable version supports most EOL versions. Some companies need very specific features of both versions, which is not possible with today's public releases. While we recommend waiting for Runtime (anticipated March release), if you need a private release, contact support@author.io to arrange a paid support contract.
coreybutler%2Fnvm-windows | Trendshift
Sponsors
 

Can't sponsor?
Consider nominating @coreybutler for a Github star.

Running into issues? See the common issues wiki.

Seeking Feedback:

We're working on Runtime (rt), the successor to NVM For Windows. Please contribute by taking a minute to complete this form. Thank you!

## Overview Manage multiple installations of node.js on a Windows computer. **tl;dr** Similar (not identical) to [nvm](https://github.com/creationix/nvm), but for Windows. Has an installer. [Download Now](https://github.com/coreybutler/nvm-windows/releases)! This has always been a node version manager, not an io.js manager, so there is no back-support for io.js. Node 4+ is supported. Remember when running `nvm install` or `nvm use`, Windows usually requires administrative rights (to create symlinks). To install the latest version of Node.js, run `nvm install latest`. To install the latest stable version, run `nvm install lts`. ![NVM for Windows](https://github.com/coreybutler/staticassets/raw/master/images/nvm-1.1.8-screenshot.jpg) There are situations where the ability to switch between different versions of Node.js can be very useful. For example, if you want to test a module you're developing with the latest bleeding edge version without uninstalling the stable version of node, this utility can help. ![Switch between stable and unstable versions.](https://github.com/coreybutler/staticassets/raw/master/images/nvm-usage-highlighted.jpg) ### Installation & Upgrades #### :star: :star: Uninstall any pre-existing Node installations!! :star: :star: The simplest (recommended) way to get NVM for Windows running properly is to uninstall any prior Node installation _before_ installing NVM for Windows. It avoids all of the pitfalls listed below. However; you may not wish to nuke your Node installation if you've highly customized it. NVM for Windows _can_ assume management of an existing installation, but there are nuances to this (dependent entirely on the permissions of the user running the installation). If you have an administrative account, it's relatively safe to install NVM for Windows before uninstalling the original Node version. If you are working in a closed environment, such as a corporate Active Directory environment where installations/uninstallations are controlled by group policy, you should really consider removing the original version of Node before installing NVM4W. _Permission Problems_ For security reasons, Windows will not allow an application from one vendor to "uninstall" an application from a different vendor. The official NVM4W installer will attempt assume management of an existing installation of Node., but it cannot actually uninstall the original Node.js version. To work around this, NVM for Windows installer attempts to copy the original Node.js installation files to the NVM root. This includes global npm modules and configurations. Once this process is complete, the original Node.js installation can be uninstalled without losing data. _PATH Installation Problems_ If you attempt to configure the `NVM_SYMLINK` to use an existing directory (like `C:\Program Files\nodejs`), it will fail because a symlink cannot overwrite a physical directory. This is not a problem if you choose a different symlink path (such as `C:\nvm\node`). _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. :eyes: **Backup any global `npmrc` config** :eyes: (e.g. `%AppData%\npm\etc\npmrc`) Alternatively, copy the settings to the user config `%UserProfile%\.npmrc`. Delete the existing npm install location (e.g. `%AppData%\npm`) to prevent global module conflicts. #### Install nvm-windows Use the [latest installer](https://github.com/coreybutler/nvm/releases) (comes with an uninstaller). Alternatively, follow the [manual installation](https://github.com/coreybutler/nvm-windows/wiki#manual-installation) guide. _If NVM4W doesn't appear to work immediately after installation, restart the terminal/powershell (not the whole computer)._ ![NVM for Windows Installer](https://github.com/coreybutler/staticassets/raw/master/images/nvm-installer.jpg) #### Reinstall any global utilities After install, reinstalling global utilities (e.g. yarn) will have to be done for each installed version of node: ``` nvm use 14.0.0 npm install -g yarn nvm use 12.0.1 npm install -g yarn ``` ### Upgrading nvm-windows :bulb: _As of v1.1.8, there is an upgrade utility that will automate the upgrade process._ **To upgrade nvm-windows**, run the new installer. It will safely overwrite the files it needs to update without touching your node.js installations. Make sure you use the same installation and symlink folder. If you originally installed to the default locations, you just need to click "next" on each window until it finishes. ### Usage **nvm-windows runs in an Admin shell**. You'll need to start `powershell` or Command Prompt as Administrator to use nvm-windows NVM for Windows is a command line tool. Simply type `nvm` in the console for help. The basic commands are: - **`nvm arch [32|64]`**: Show if node is running in 32 or 64 bit mode. Specify 32 or 64 to override the default architecture. - **`nvm debug`**: Check the NVM4W process for known problems. - **`nvm current`**: Display active version. - **`nvm install [arch]`**: The version can be a specific version, "latest" for the latest current version, or "lts" for the most recent LTS version. Optionally specify whether to install the 32 or 64 bit version (defaults to system arch). Set [arch] to "all" to install 32 AND 64 bit versions. Add `--insecure` to the end of this command to bypass SSL validation of the remote download server. - **`nvm list [available]`**: List the node.js installations. Type `available` at the end to show a list of versions available for download. - **`nvm on`**: Enable node.js version management. - **`nvm off`**: Disable node.js version management (does not uninstall anything). - **`nvm proxy [url]`**: Set a proxy to use for downloads. Leave `[url]` blank to see the current proxy. Set `[url]` to "none" to remove the proxy. - **`nvm uninstall `**: Uninstall a specific version. - **`nvm use [arch]`**: Switch to use the specified version. Optionally use `latest`, `lts`, or `newest`. `newest` is the latest _installed_ version. Optionally specify 32/64bit architecture. `nvm use ` will continue using the selected version, but switch to 32/64 bit mode. For information about using `use` in a specific directory (or using `.nvmrc`), please refer to [issue #16](https://github.com/coreybutler/nvm-windows/issues/16). - **`nvm root `**: Set the directory where nvm should store different versions of node.js. If `` is not set, the current root will be displayed. - **`nvm version`**: Displays the current running version of NVM for Windows. - **`nvm node_mirror `**: Set the node mirror.People in China can use *https://npmmirror.com/mirrors/node/* - **`nvm npm_mirror `**: Set the npm mirror.People in China can use *https://npmmirror.com/mirrors/npm/* ### :warning: Gotcha! Please note that any global npm modules you may have installed are **not** shared between the various versions of node.js you have installed. Additionally, some npm modules may not be supported in the version of node you're using, so be aware of your environment as you work. ### :name_badge: Antivirus Users have reported some problems using antivirus, specifically McAfee. It appears the antivirus software is manipulating access to the VBScript engine. See [issue #133](https://github.com/coreybutler/nvm-windows/issues/133) for details and resolution. **v1.1.8 is not code signed**, but all other versions are signed by [Ecor Ventures LLC](https://ecorventures.com)/[Author.io](https://author.io). This should help prevent false positives with most antivirus software. > v1.1.8+ was not code signed due to an expired certificate (see the [release notes](https://github.com/coreybutler/nvm-windows/releases/tag/1.1.8) for reasons). **v1.1.9 _is_ code signed** thanks to [ajyong](https://github.com/ajyong), who sponsored the new certificate. ### Using Yarn **tldr;** `npm i -g yarn` See the [wiki](https://github.com/coreybutler/nvm-windows/wiki/Common-Issues#how-do-i-use-yarn-with-nvm-windows) for details. ### Build from source - Install go from http://golang.org - Download source / Git Clone the repo - Change GOARCH to amd64 in build.bat if you feel like building a 64-bit executable - Fire up a Windows command prompt and change directory to project dir - Execute `go get github.com/blang/semver` - Execute `go get github.com/olekukonko/tablewriter` - Execute `build.bat` - Check the `dist`directory for generated setup program. --- ## :bulb: Why another version manager? There are several version managers for node.js. Tools like [nvm](https://github.com/creationix/nvm) and [n](https://github.com/tj/n) only run on Mac OSX and Linux. Windows users are left in the cold? No. [nvmw](https://github.com/hakobera/nvmw) and [nodist](https://github.com/marcelklehr/nodist) are both designed for Windows. So, why another version manager for Windows? The architecture of most node version managers for Windows rely on `.bat` files, which do some clever tricks to set or mimic environment variables. Some of them use node itself (once it's downloaded), which is admirable, but prone to problems. Right around node 0.10.30, the installation structure changed a little, causing some of these to just stop working with anything new. Additionally, some users struggle to install these modules since it requires a little more knowledge of node's installation structure. I believe if it were easier for people to switch between versions, people might take the time to test their code on back and future versions... which is just good practice. ## :bulb: What's the big difference? First and foremost, this version of nvm has no dependency on node. It's written in [Go](https://golang.org/), which is a much more structured approach than hacking around a limited `.bat` file. It does not rely on having an existing node installation. Go offers the ability to create a Mac/Linux version on the same code base. In fact, this is already underway. The control mechanism is also quite different. There are two general ways to support multiple node installations with hot switching capabilities. The first is to modify the system `PATH` any time you switch versions, or bypass it by using a `.bat` file to mimic the node executable and redirect accordingly. This always seemed a little hackish to me, and there are some quirks as a result of this implementation. The second option is to use a symlink. This concept requires putting the symlink in the system `PATH`, then updating its target to the node installation directory you want to use. This is a straightforward approach, and seems to be what people recommend.... until they realize just how much of a pain symlinks are on Windows. This is why it hasn't happened before. In order to create/modify a symlink, you must be running as an admin, and you must get around Windows UAC (that annoying prompt). Luckily, this is a challenge I already solved with some helper scripts in [node-windows](https://github.com/coreybutler/node-windows). As a result, NVM for Windows maintains a single symlink that is put in the system `PATH` during installation only. Switching to different versions of node is a matter of switching the symlink target. As a result, this utility does **not** require you to run `nvm use x.x.x` every time you open a console window. When you _do_ run `nvm use x.x.x`, the active version of node is automatically updated across all open console windows. It also persists between system reboots, so you only need to use nvm when you want to make a change. NVM for Windows comes with an installer, courtesy of a byproduct of my work on [Fenix Web Server](https://preview.fenixwebserver.com). Overall, this project brings together some ideas, a few battle-hardened pieces of other modules, and support for newer versions of node. NVM for Windows recognizes the "latest" versions using a [list](https://nodejs.org/download/release/index.json) provided by the Node project. Version 1.1.1+ use this list. Before this list existed, I was scraping releases and serving it as a standalone [data feed](https://github.com/coreybutler/nodedistro). This list was used in versions 1.1.0 and prior, but is now deprecated. ## Motivation I needed it, plain and simple. Additionally, it's apparent that [support for multiple versions](https://github.com/nodejs/node-v0.x-archive/issues/8075) is not coming to node core. It was also an excuse to play with Go. ## Why Go? Why not Node? I chose Go because it is cross-platform, felt like less overhead than Java, has been around longer than most people think. Plus, I wanted to experiment with it. I've been asked why I didn't write it with Node. Trying to write a tool with the tool you're trying to install doesn't make sense to me. As a result, my project requirements for this were simple... something that's not Node. Node will continue to evolve and change. If you need a reminder of that, remember io.js, Ayo, all the breaking changes between 4.x.x and 6.x.x, and the shift to ES Modules in 12+. Change is inevitable in the world of software. JavaScript is extremely dynamic. ## :pray: Thanks Thanks to everyone who has submitted issues on and off Github, made suggestions, and generally helped make this a better project. Special thanks to - [@vkbansal](https://github.com/vkbansal), who provided significant early feedback throughout the early releases. - [@rainabba](https://github.com/rainabba) and [@sullivanpt](https://github.com/sullivanpt) for getting Node v4 support integrated. - [@s-h-a-d-o-w](https://github.com/s-h-a-d-o-w) who resolved the longstanding space escaping issue in path names ([#355](https://github.com/coreybutler/nvm-windows/pull/355)). - [ajyong](https://github.com/ajyong) who sponsored the code signing certificate in late 2021.
![Contributors](https://contrib.rocks/image?repo=coreybutler/nvm-windows) ================================================ FILE: SUPPORT.md ================================================ # Need Help? If you're running into problems using NVM4W, try the following resources. 1. Review the [wiki](https://github.com/coreybutler/nvm-windows/wiki). 1. Join ongoing [discussions](https://github.com/coreybutler/nvm-windows/discussions). 1. Search [closed issues](https://github.com/coreybutler/nvm-windows/issues?q=is%3Aissue+is%3Aclosed) for existing solutions. 1. Search [StackOverflow ([nvm-windows])](https://stackoverflow.com/questions/tagged/nvm-windows) for answers. There are also a number of [YouTube videos](https://www.youtube.com/results?search_query=NVM+for+Windows) and blogs online that show how to use NVM for Windows. If none of these things help and you suspect a bug, file an issue. You can also try reaching out to [@goldglovecb](https://twitter.com/goldglovecb) on Twitter. ================================================ FILE: assets/buildtools/Default.isl ================================================ ; *** Inno Setup version 6.1.0+ English messages *** ; ; To download user-contributed translations of this file, go to: ; https://jrsoftware.org/files/istrans/ ; ; Note: When translating this text, do not add periods (.) to the end of ; messages that didn't have them already, because on those messages Inno ; Setup adds the periods automatically (appending a period would result in ; two periods being displayed). [LangOptions] ; The following three entries are very important. Be sure to read and ; understand the '[LangOptions] section' topic in the help file. LanguageName=English LanguageID=$0409 LanguageCodePage=0 ; If the language you are translating to requires special font faces or ; sizes, uncomment any of the following entries and change them accordingly. ;DialogFontName= ;DialogFontSize=8 ;WelcomeFontName=Verdana ;WelcomeFontSize=12 ;TitleFontName=Arial ;TitleFontSize=29 ;CopyrightFontName=Arial ;CopyrightFontSize=8 [Messages] ; *** Application titles SetupAppTitle=Setup SetupWindowTitle=Setup - %1 UninstallAppTitle=Uninstall UninstallAppFullTitle=%1 Uninstall ; *** Misc. common InformationTitle=Information ConfirmTitle=Confirm ErrorTitle=Error ; *** SetupLdr messages SetupLdrStartupMessage=This will install %1. Do you wish to continue? LdrCannotCreateTemp=Unable to create a temporary file. Setup aborted LdrCannotExecTemp=Unable to execute file in the temporary directory. Setup aborted HelpTextNote= ; *** Startup error messages LastErrorMessage=%1.%n%nError %2: %3 SetupFileMissing=The file %1 is missing from the installation directory. Please correct the problem or obtain a new copy of the program. SetupFileCorrupt=The setup files are corrupted. Please obtain a new copy of the program. SetupFileCorruptOrWrongVer=The setup files are corrupted, or are incompatible with this version of Setup. Please correct the problem or obtain a new copy of the program. InvalidParameter=An invalid parameter was passed on the command line:%n%n%1 SetupAlreadyRunning=Setup is already running. WindowsVersionNotSupported=This program does not support the version of Windows your computer is running. WindowsServicePackRequired=This program requires %1 Service Pack %2 or later. NotOnThisPlatform=This program will not run on %1. OnlyOnThisPlatform=This program must be run on %1. OnlyOnTheseArchitectures=This program can only be installed on versions of Windows designed for the following processor architectures:%n%n%1 WinVersionTooLowError=This program requires %1 version %2 or later. WinVersionTooHighError=This program cannot be installed on %1 version %2 or later. AdminPrivilegesRequired=You must be logged in as an administrator when installing this program. PowerUserPrivilegesRequired=You must be logged in as an administrator or as a member of the Power Users group when installing this program. SetupAppRunningError=Setup has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit. UninstallAppRunningError=Uninstall has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit. ; *** Startup questions PrivilegesRequiredOverrideTitle=Select Setup Install Mode PrivilegesRequiredOverrideInstruction=Select install mode PrivilegesRequiredOverrideText1=%1 can be installed for all users (requires administrative privileges), or for you only. PrivilegesRequiredOverrideText2=%1 can be installed for you only, or for all users (requires administrative privileges). PrivilegesRequiredOverrideAllUsers=Install for &all users PrivilegesRequiredOverrideAllUsersRecommended=Install for &all users (recommended) PrivilegesRequiredOverrideCurrentUser=Install for &me only PrivilegesRequiredOverrideCurrentUserRecommended=Install for &me only (recommended) ; *** Misc. errors ErrorCreatingDir=Setup was unable to create the directory "%1" ErrorTooManyFilesInDir=Unable to create a file in the directory "%1" because it contains too many files ; *** Setup common messages ExitSetupTitle=Exit Setup ExitSetupMessage=Setup is not complete. If you exit now, the program will not be installed.%n%nYou may run Setup again at another time to complete the installation.%n%nExit Setup? AboutSetupMenuItem=&About Setup... AboutSetupTitle=About Setup AboutSetupMessage=%1 version %2%n%3%n%n%1 home page:%n%4 AboutSetupNote= TranslatorNote= ; *** Buttons ButtonBack=< &Back ButtonNext=&Next > ButtonInstall=&Install ButtonOK=OK ButtonCancel=Cancel ButtonYes=&Yes ButtonYesToAll=Yes to &All ButtonNo=&No ButtonNoToAll=N&o to All ButtonFinish=&Finish ButtonBrowse=&Browse... ButtonWizardBrowse=B&rowse... ButtonNewFolder=&Make New Folder ; *** "Select Language" dialog messages SelectLanguageTitle=Select Setup Language SelectLanguageLabel=Select the language to use during the installation. ; *** Common wizard text ClickNext=Click Next to continue, or Cancel to exit Setup. BeveledLabel= BrowseDialogTitle=Browse For Folder BrowseDialogLabel=Select a folder in the list below, then click OK. NewFolderName=New Folder ; *** "Welcome" wizard page WelcomeLabel1=Welcome to the [name] Setup Wizard WelcomeLabel2=This will install [name/ver] on your computer.%n%nIt is recommended that you close all other applications before continuing. ; *** "Password" wizard page WizardPassword=Password PasswordLabel1=This installation is password protected. PasswordLabel3=Please provide the password, then click Next to continue. Passwords are case-sensitive. PasswordEditLabel=&Password: IncorrectPassword=The password you entered is not correct. Please try again. ; *** "License Agreement" wizard page WizardLicense=License Agreement LicenseLabel=Please read the following important information before continuing. LicenseLabel3=Please read the following License Agreement. You must accept the terms of this agreement before continuing with the installation. LicenseAccepted=I &accept the agreement LicenseNotAccepted=I &do not accept the agreement ; *** "Information" wizard pages WizardInfoBefore=Information InfoBeforeLabel=Please read the following important information before continuing. InfoBeforeClickLabel=When you are ready to continue with Setup, click Next. WizardInfoAfter=Information InfoAfterLabel=Please read the following important information before continuing. InfoAfterClickLabel=When you are ready to continue with Setup, click Next. ; *** "User Information" wizard page WizardUserInfo=User Information UserInfoDesc=Please enter your information. UserInfoName=&User Name: UserInfoOrg=&Organization: UserInfoSerial=&Serial Number: UserInfoNameRequired=You must enter a name. ; *** "Select Destination Location" wizard page WizardSelectDir=Select Destination Location SelectDirDesc=Where should [name] be installed? SelectDirLabel3=Setup will install [name] into the following folder. SelectDirBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse. DiskSpaceGBLabel=At least [gb] GB of free disk space is required. DiskSpaceMBLabel=At least [mb] MB of free disk space is required. CannotInstallToNetworkDrive=Setup cannot install to a network drive. CannotInstallToUNCPath=Setup cannot install to a UNC path. InvalidPath=You must enter a full path with drive letter; for example:%n%nC:\APP%n%nor a UNC path in the form:%n%n\\server\share InvalidDrive=The drive or UNC share you selected does not exist or is not accessible. Please select another. DiskSpaceWarningTitle=Not Enough Disk Space DiskSpaceWarning=Setup requires at least %1 KB of free space to install, but the selected drive only has %2 KB available.%n%nDo you want to continue anyway? DirNameTooLong=The folder name or path is too long. InvalidDirName=The folder name is not valid. BadDirName32=Folder names cannot include any of the following characters:%n%n%1 DirExistsTitle=Folder Exists DirExists=The folder:%n%n%1%n%nalready exists. Would you like to install to that folder anyway? DirDoesntExistTitle=Folder Does Not Exist DirDoesntExist=The folder:%n%n%1%n%ndoes not exist. Would you like the folder to be created? ; *** "Select Components" wizard page WizardSelectComponents=Select Components SelectComponentsDesc=Which components should be installed? SelectComponentsLabel2=Select the components you want to install; clear the components you do not want to install. Click Next when you are ready to continue. FullInstallation=Full installation ; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) CompactInstallation=Compact installation CustomInstallation=Custom installation NoUninstallWarningTitle=Components Exist NoUninstallWarning=Setup has detected that the following components are already installed on your computer:%n%n%1%n%nDeselecting these components will not uninstall them.%n%nWould you like to continue anyway? ComponentSize1=%1 KB ComponentSize2=%1 MB ComponentsDiskSpaceGBLabel=Current selection requires at least [gb] GB of disk space. ComponentsDiskSpaceMBLabel=Current selection requires at least [mb] MB of disk space. ; *** "Select Additional Tasks" wizard page WizardSelectTasks=Select Additional Tasks SelectTasksDesc=Which additional tasks should be performed? SelectTasksLabel2=Select the additional tasks you would like Setup to perform while installing [name], then click Next. ; *** "Select Start Menu Folder" wizard page WizardSelectProgramGroup=Select Start Menu Folder SelectStartMenuFolderDesc=Where should Setup place the program's shortcuts? SelectStartMenuFolderLabel3=Setup will create the program's shortcuts in the following Start Menu folder. SelectStartMenuFolderBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse. MustEnterGroupName=You must enter a folder name. GroupNameTooLong=The folder name or path is too long. InvalidGroupName=The folder name is not valid. BadGroupName=The folder name cannot include any of the following characters:%n%n%1 NoProgramGroupCheck2=&Don't create a Start Menu folder ; *** "Ready to Install" wizard page WizardReady=Ready to Install ReadyLabel1=Setup is now ready to begin installing [name] on your computer. ReadyLabel2a=Click Install to continue with the installation, or click Back if you want to review or change any settings. ReadyLabel2b=Click Install to continue with the installation. ReadyMemoUserInfo=User information: ReadyMemoDir=Destination location: ReadyMemoType=Setup type: ReadyMemoComponents=Selected components: ReadyMemoGroup=Start Menu folder: ReadyMemoTasks=Additional tasks: ; *** TDownloadWizardPage wizard page and DownloadTemporaryFile DownloadingLabel=Downloading additional files... ButtonStopDownload=&Stop download StopDownload=Are you sure you want to stop the download? ErrorDownloadAborted=Download aborted ErrorDownloadFailed=Download failed: %1 %2 ErrorDownloadSizeFailed=Getting size failed: %1 %2 ErrorFileHash1=File hash failed: %1 ErrorFileHash2=Invalid file hash: expected %1, found %2 ErrorProgress=Invalid progress: %1 of %2 ErrorFileSize=Invalid file size: expected %1, found %2 ; *** "Preparing to Install" wizard page WizardPreparing=Preparing to Install PreparingDesc=Setup is preparing to install [name] on your computer. PreviousInstallNotCompleted=The installation/removal of a previous program was not completed. You will need to restart your computer to complete that installation.%n%nAfter restarting your computer, run Setup again to complete the installation of [name]. CannotContinue=Setup cannot continue. Please click Cancel to exit. ApplicationsFound=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications. ApplicationsFound2=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications. After the installation has completed, Setup will attempt to restart the applications. CloseApplications=&Automatically close the applications DontCloseApplications=&Do not close the applications ErrorCloseApplications=Setup was unable to automatically close all applications. It is recommended that you close all applications using files that need to be updated by Setup before continuing. PrepareToInstallNeedsRestart=Setup must restart your computer. After restarting your computer, run Setup again to complete the installation of [name].%n%nWould you like to restart now? ; *** "Installing" wizard page WizardInstalling=Installing InstallingLabel=Please wait while Setup installs [name] on your computer. ; *** "Setup Completed" wizard page FinishedHeadingLabel=Completing the [name] Setup Wizard FinishedLabelNoIcons=Setup has finished installing [name] on your computer. FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed shortcuts. ClickFinish=Click Finish to exit Setup. FinishedRestartLabel=To complete the installation of [name], Setup must restart your computer. Would you like to restart now? FinishedRestartMessage=To complete the installation of [name], Setup must restart your computer.%n%nWould you like to restart now? ShowReadmeCheck=Yes, I would like to view the README file YesRadio=&Yes, restart the computer now NoRadio=&No, I will restart the computer later ; used for example as 'Run MyProg.exe' RunEntryExec=Run %1 ; used for example as 'View Readme.txt' RunEntryShellExec=View %1 ; *** "Setup Needs the Next Disk" stuff ChangeDiskTitle=Setup Needs the Next Disk SelectDiskLabel2=Please insert Disk %1 and click OK.%n%nIf the files on this disk can be found in a folder other than the one displayed below, enter the correct path or click Browse. PathLabel=&Path: FileNotInDir2=The file "%1" could not be located in "%2". Please insert the correct disk or select another folder. SelectDirectoryLabel=Please specify the location of the next disk. ; *** Installation phase messages SetupAborted=Setup was not completed.%n%nPlease correct the problem and run Setup again. AbortRetryIgnoreSelectAction=Select action AbortRetryIgnoreRetry=&Try again AbortRetryIgnoreIgnore=&Ignore the error and continue AbortRetryIgnoreCancel=Cancel installation ; *** Installation status messages StatusClosingApplications=Closing applications... StatusCreateDirs=Creating directories... StatusExtractFiles=Extracting files... StatusCreateIcons=Creating shortcuts... StatusCreateIniEntries=Creating INI entries... StatusCreateRegistryEntries=Creating registry entries... StatusRegisterFiles=Registering files... StatusSavingUninstall=Saving uninstall information... StatusRunProgram=Finishing installation... StatusRestartingApplications=Restarting applications... StatusRollback=Rolling back changes... ; *** Misc. errors ErrorInternal2=Internal error: %1 ErrorFunctionFailedNoCode=%1 failed ErrorFunctionFailed=%1 failed; code %2 ErrorFunctionFailedWithMessage=%1 failed; code %2.%n%3 ErrorExecutingProgram=Unable to execute file:%n%1 ; *** Registry errors ErrorRegOpenKey=Error opening registry key:%n%1\%2 ErrorRegCreateKey=Error creating registry key:%n%1\%2 ErrorRegWriteKey=Error writing to registry key:%n%1\%2 ; *** INI errors ErrorIniEntry=Error creating INI entry in file "%1". ; *** File copying errors FileAbortRetryIgnoreSkipNotRecommended=&Skip this file (not recommended) FileAbortRetryIgnoreIgnoreNotRecommended=&Ignore the error and continue (not recommended) SourceIsCorrupted=The source file is corrupted SourceDoesntExist=The source file "%1" does not exist ExistingFileReadOnly2=The existing file could not be replaced because it is marked read-only. ExistingFileReadOnlyRetry=&Remove the read-only attribute and try again ExistingFileReadOnlyKeepExisting=&Keep the existing file ErrorReadingExistingDest=An error occurred while trying to read the existing file: FileExistsSelectAction=Select action FileExists2=The file already exists. FileExistsOverwriteExisting=&Overwrite the existing file FileExistsKeepExisting=&Keep the existing file FileExistsOverwriteOrKeepAll=&Do this for the next conflicts ExistingFileNewerSelectAction=Select action ExistingFileNewer2=The existing file is newer than the one Setup is trying to install. ExistingFileNewerOverwriteExisting=&Overwrite the existing file ExistingFileNewerKeepExisting=&Keep the existing file (recommended) ExistingFileNewerOverwriteOrKeepAll=&Do this for the next conflicts ErrorChangingAttr=An error occurred while trying to change the attributes of the existing file: ErrorCreatingTemp=An error occurred while trying to create a file in the destination directory: ErrorReadingSource=An error occurred while trying to read the source file: ErrorCopying=An error occurred while trying to copy a file: ErrorReplacingExistingFile=An error occurred while trying to replace the existing file: ErrorRestartReplace=RestartReplace failed: ErrorRenamingTemp=An error occurred while trying to rename a file in the destination directory: ErrorRegisterServer=Unable to register the DLL/OCX: %1 ErrorRegSvr32Failed=RegSvr32 failed with exit code %1 ErrorRegisterTypeLib=Unable to register the type library: %1 ; *** Uninstall display name markings ; used for example as 'My Program (32-bit)' UninstallDisplayNameMark=%1 (%2) ; used for example as 'My Program (32-bit, All users)' UninstallDisplayNameMarks=%1 (%2, %3) UninstallDisplayNameMark32Bit=32-bit UninstallDisplayNameMark64Bit=64-bit UninstallDisplayNameMarkAllUsers=All users UninstallDisplayNameMarkCurrentUser=Current user ; *** Post-installation errors ErrorOpeningReadme=An error occurred while trying to open the README file. ErrorRestartingComputer=Setup was unable to restart the computer. Please do this manually. ; *** Uninstaller messages UninstallNotFound=File "%1" does not exist. Cannot uninstall. UninstallOpenError=File "%1" could not be opened. Cannot uninstall UninstallUnsupportedVer=The uninstall log file "%1" is in a format not recognized by this version of the uninstaller. Cannot uninstall UninstallUnknownEntry=An unknown entry (%1) was encountered in the uninstall log ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components? UninstallOnlyOnWin64=This installation can only be uninstalled on 64-bit Windows. OnlyAdminCanUninstall=This installation can only be uninstalled by a user with administrative privileges. UninstallStatusLabel=Please wait while %1 is removed from your computer. UninstalledAll=%1 was successfully removed from your computer. UninstalledMost=%1 uninstall complete.%n%nSome elements could not be removed. These can be removed manually. UninstalledAndNeedsRestart=To complete the uninstallation of %1, your computer must be restarted.%n%nWould you like to restart now? UninstallDataCorrupted="%1" file is corrupted. Cannot uninstall ; *** Uninstallation phase messages ConfirmDeleteSharedFileTitle=Remove Shared File? ConfirmDeleteSharedFile2=The system indicates that the following shared file is no longer in use by any programs. Would you like for Uninstall to remove this shared file?%n%nIf any programs are still using this file and it is removed, those programs may not function properly. If you are unsure, choose No. Leaving the file on your system will not cause any harm. SharedFileNameLabel=File name: SharedFileLocationLabel=Location: WizardUninstalling=Uninstall Status StatusUninstalling=Uninstalling %1... ; *** Shutdown block reasons ShutdownBlockReasonInstallingApp=Installing %1. ShutdownBlockReasonUninstallingApp=Uninstalling %1. ; The custom messages below aren't used by Setup itself, but if you make ; use of them in your scripts, you'll want to translate them. [CustomMessages] NameAndVersion=%1 version %2 AdditionalIcons=Additional shortcuts: CreateDesktopIcon=Create a &desktop shortcut CreateQuickLaunchIcon=Create a &Quick Launch shortcut ProgramOnTheWeb=%1 on the Web UninstallProgram=Uninstall %1 LaunchProgram=Launch %1 AssocFileExtension=&Associate %1 with the %2 file extension AssocingFileExtension=Associating %1 with the %2 file extension... AutoStartProgramGroupDescription=Startup: AutoStartProgram=Automatically start %1 AddonHostProgramNotFound=%1 could not be located in the folder you selected.%n%nDo you want to continue anyway? ================================================ FILE: assets/buildtools/ISPPBuiltins.iss ================================================ // Inno Setup Preprocessor // // Inno Setup (C) 1997-2020 Jordan Russell. All Rights Reserved. // Portions Copyright (C) 2000-2020 Martijn Laan. All Rights Reserved. // Portions Copyright (C) 2001-2004 Alex Yackimoff. All Rights Reserved. // // See the ISPP help file for more documentation of the functions defined by this file // #if defined(ISPP_INVOKED) && !defined(_BUILTINS_ISS_) // #if PREPROCVER < 0x01000000 # error Inno Setup Preprocessor version is outdated #endif // #define _BUILTINS_ISS_ // #ifdef __OPT_E__ # define private EnableOptE # pragma option -e- #endif #ifndef __POPT_P__ # define private DisablePOptP #else # pragma parseroption -p- #endif #define NewLine "\n" #define Tab "\t" #pragma parseroption -p+ #pragma spansymbol "\" #define True 1 #define False 0 #define Yes True #define No False #define MaxInt 0x7FFFFFFFFFFFFFFFL #define MinInt 0x8000000000000000L #define NULL #define void // TypeOf constants #define TYPE_ERROR 0 #define TYPE_NULL 1 #define TYPE_INTEGER 2 #define TYPE_STRING 3 #define TYPE_MACRO 4 #define TYPE_FUNC 5 #define TYPE_ARRAY 6 // Helper macro to find out the type of an array element or expression. TypeOf // standard function only allows identifier as its parameter. Use this macro // to convert an expression to identifier. #define TypeOf2(any Expr) TypeOf(Expr) // ReadReg constants #define HKEY_CLASSES_ROOT 0x80000000UL #define HKEY_CURRENT_USER 0x80000001UL #define HKEY_LOCAL_MACHINE 0x80000002UL #define HKEY_USERS 0x80000003UL #define HKEY_CURRENT_CONFIG 0x80000005UL #define HKEY_CLASSES_ROOT_64 0x82000000UL #define HKEY_CURRENT_USER_64 0x82000001UL #define HKEY_LOCAL_MACHINE_64 0x82000002UL #define HKEY_USERS_64 0x82000003UL #define HKEY_CURRENT_CONFIG_64 0x82000005UL #define HKCR HKEY_CLASSES_ROOT #define HKCU HKEY_CURRENT_USER #define HKLM HKEY_LOCAL_MACHINE #define HKU HKEY_USERS #define HKCC HKEY_CURRENT_CONFIG #define HKCR64 HKEY_CLASSES_ROOT_64 #define HKCU64 HKEY_CURRENT_USER_64 #define HKLM64 HKEY_LOCAL_MACHINE_64 #define HKU64 HKEY_USERS_64 #define HKCC64 HKEY_CURRENT_CONFIG_64 // Exec constants #define SW_HIDE 0 #define SW_SHOWNORMAL 1 #define SW_NORMAL 1 #define SW_SHOWMINIMIZED 2 #define SW_SHOWMAXIMIZED 3 #define SW_MAXIMIZE 3 #define SW_SHOWNOACTIVATE 4 #define SW_SHOW 5 #define SW_MINIMIZE 6 #define SW_SHOWMINNOACTIVE 7 #define SW_SHOWNA 8 #define SW_RESTORE 9 #define SW_SHOWDEFAULT 10 #define SW_MAX 10 // Find constants #define FIND_MATCH 0x00 #define FIND_BEGINS 0x01 #define FIND_ENDS 0x02 #define FIND_CONTAINS 0x03 #define FIND_CASESENSITIVE 0x04 #define FIND_SENSITIVE FIND_CASESENSITIVE #define FIND_AND 0x00 #define FIND_OR 0x08 #define FIND_NOT 0x10 #define FIND_TRIM 0x20 // FindFirst constants #define faReadOnly 0x00000001 #define faHidden 0x00000002 #define faSysFile 0x00000004 #define faVolumeID 0x00000008 #define faDirectory 0x00000010 #define faArchive 0x00000020 #define faSymLink 0x00000040 #define faAnyFile 0x0000003F // GetStringFileInfo standard names #define COMPANY_NAME "CompanyName" #define FILE_DESCRIPTION "FileDescription" #define FILE_VERSION "FileVersion" #define INTERNAL_NAME "InternalName" #define LEGAL_COPYRIGHT "LegalCopyright" #define ORIGINAL_FILENAME "OriginalFilename" #define PRODUCT_NAME "ProductName" #define PRODUCT_VERSION "ProductVersion" // GetStringFileInfo helpers #define GetFileCompany(str FileName) GetStringFileInfo(FileName, COMPANY_NAME) #define GetFileDescription(str FileName) GetStringFileInfo(FileName, FILE_DESCRIPTION) #define GetFileVersionString(str FileName) GetStringFileInfo(FileName, FILE_VERSION) #define GetFileCopyright(str FileName) GetStringFileInfo(FileName, LEGAL_COPYRIGHT) #define GetFileOriginalFilename(str FileName) GetStringFileInfo(FileName, ORIGINAL_FILENAME) #define GetFileProductVersion(str FileName) GetStringFileInfo(FileName, PRODUCT_VERSION) #define DeleteToFirstPeriod(str *S) \ Local[1] = Copy(S, 1, (Local[0] = Pos(".", S)) - 1), \ S = Copy(S, Local[0] + 1), \ Local[1] #define GetVersionComponents(str FileName, *Major, *Minor, *Rev, *Build) \ Local[1] = Local[0] = GetVersionNumbersString(FileName), \ Local[1] == "" ? "" : ( \ Major = Int(DeleteToFirstPeriod(Local[1])), \ Minor = Int(DeleteToFirstPeriod(Local[1])), \ Rev = Int(DeleteToFirstPeriod(Local[1])), \ Build = Int(Local[1]), \ Local[0]) #define GetPackedVersion(str FileName, *Version) \ Local[0] = GetVersionComponents(FileName, Local[1], Local[2], Local[3], Local[4]), \ Version = PackVersionComponents(Local[1], Local[2], Local[3], Local[4]), \ Local[0] #define GetVersionNumbers(str FileName, *MS, *LS) \ Local[0] = GetPackedVersion(FileName, Local[1]), \ UnpackVersionNumbers(Local[1], MS, LS), \ Local[0] #define PackVersionNumbers(int VersionMS, int VersionLS) \ VersionMS << 32 | (VersionLS & 0xFFFFFFFF) #define PackVersionComponents(int Major, int Minor, int Rev, int Build) \ Major << 48 | (Minor & 0xFFFF) << 32 | (Rev & 0xFFFF) << 16 | (Build & 0xFFFF) #define UnpackVersionNumbers(int Version, *VersionMS, *VersionLS) \ VersionMS = Version >> 32, \ VersionLS = Version & 0xFFFFFFFF, \ void #define UnpackVersionComponents(int Version, *Major, *Minor, *Rev, *Build) \ Major = Version >> 48, \ Minor = (Version >> 32) & 0xFFFF, \ Rev = (Version >> 16) & 0xFFFF, \ Build = Version & 0xFFFF, \ void #define VersionToStr(int Version) \ Str(Version >> 48 & 0xFFFF) + "." + Str(Version >> 32 & 0xFFFF) + "." + \ Str(Version >> 16 & 0xFFFF) + "." + Str(Version & 0xFFFF) #define EncodeVer(int Major, int Minor, int Revision = 0, int Build = -1) \ (Major & 0xFF) << 24 | (Minor & 0xFF) << 16 | (Revision & 0xFF) << 8 | (Build >= 0 ? Build & 0xFF : 0) #define DecodeVer(int Version, int Digits = 3) \ Str(Version >> 24 & 0xFF) + (Digits > 1 ? "." : "") + \ (Digits > 1 ? \ Str(Version >> 16 & 0xFF) + (Digits > 2 ? "." : "") : "") + \ (Digits > 2 ? \ Str(Version >> 8 & 0xFF) + (Digits > 3 && (Local = Version & 0xFF) ? "." : "") : "") + \ (Digits > 3 && Local ? \ Str(Version & 0xFF) : "") #define FindSection(str Section = "Files") \ Find(0, "[" + Section + "]", FIND_MATCH | FIND_TRIM) + 1 #if VER >= 0x03000000 # define FindNextSection(int Line) \ Find(Line, "[", FIND_BEGINS | FIND_TRIM, "]", FIND_ENDS | FIND_AND) # define FindSectionEnd(str Section = "Files") \ FindNextSection(FindSection(Section)) #else # define FindSectionEnd(str Section = "Files") \ FindSection(Section) + EntryCount(Section) #endif #define FindCode() \ Local[1] = FindSection("Code"), \ Local[0] = Find(Local[1] - 1, "program", FIND_BEGINS, ";", FIND_ENDS | FIND_AND), \ (Local[0] < 0 ? Local[1] : Local[0] + 1) #define ExtractFilePath(str PathName) \ (Local[0] = \ !(Local[1] = RPos("\", PathName)) ? \ "" : \ Copy(PathName, 1, Local[1] - 1)), \ Local[0] + \ ((Local[2] = Len(Local[0])) == 2 && Copy(Local[0], Local[2]) == ":" ? \ "\" : \ "") #define ExtractFileDir(str PathName) \ RemoveBackslash(ExtractFilePath(PathName)) #define ExtractFileExt(str PathName) \ Local[0] = RPos(".", PathName), \ Copy(PathName, Local[0] + 1) #define ExtractFileName(str PathName) \ !(Local[0] = RPos("\", PathName)) ? \ PathName : \ Copy(PathName, Local[0] + 1) #define ChangeFileExt(str FileName, str NewExt) \ !(Local[0] = RPos(".", FileName)) ? \ FileName + "." + NewExt : \ Copy(FileName, 1, Local[0]) + NewExt #define RemoveFileExt(str FileName) \ !(Local[0] = RPos(".", FileName)) ? \ FileName : \ Copy(FileName, 1, Local[0] - 1) #define AddBackslash(str S) \ Copy(S, Len(S)) == "\" ? S : S + "\" #define RemoveBackslash(str S) \ Local[0] = Len(S), \ Local[0] > 0 ? \ Copy(S, Local[0]) == "\" ? \ (Local[0] == 3 && Copy(S, 2, 1) == ":" ? \ S : \ Copy(S, 1, Local[0] - 1)) : \ S : \ "" #define Delete(str *S, int Index, int Count = MaxInt) \ S = Copy(S, 1, Index - 1) + Copy(S, Index + Count) #define Insert(str *S, int Index, str Substr) \ Index > Len(S) + 1 ? \ S : \ S = Copy(S, 1, Index - 1) + SubStr + Copy(S, Index) #define YesNo(str S) \ (S = LowerCase(S)) == "yes" || S == "true" || S == "1" #define IsDirSet(str SetupDirective) \ YesNo(SetupSetting(SetupDirective)) #define Power(int X, int P = 2) \ !P ? 1 : X * Power(X, P - 1) #define Min(int A, int B, int C = MaxInt) \ A < B ? A < C ? Int(A) : Int(C) : Int(B) #define Max(int A, int B, int C = MinInt) \ A > B ? A > C ? Int(A) : Int(C) : Int(B) #define SameText(str S1, str S2) \ LowerCase(S1) == LowerCase(S2) #define SameStr(str S1, str S2) \ S1 == S2 #define WarnRenamedVersion(str OldName, str NewName) \ Warning("Function """ + OldName + """ has been renamed. Use """ + NewName + """ instead.") #define ParseVersion(str FileName, *Major, *Minor, *Rev, *Build) \ WarnRenamedVersion("ParseVersion", "GetVersionComponents"), \ GetVersionComponents(FileName, Major, Minor, Rev, Build) #define GetFileVersion(str FileName) \ WarnRenamedVersion("GetFileVersion", "GetVersionNumbersString"), \ GetVersionNumbersString(FileName) #ifdef DisablePOptP # pragma parseroption -p- #endif #ifdef EnableOptE # pragma option -e+ #endif #endif ================================================ FILE: assets/elevate.cmd ================================================ @setlocal @echo off :: Try without elevation, in case %NVM_SYMLINK% is a user-owned path and the :: machine has Windows 10 Developer Mode enabled %* if %ERRORLEVEL% LSS 1 goto :EOF :: The command failed without elevation, try with elevation set CMD=%* set APP=%1 start wscript //nologo "%~dpn0.vbs" %* ================================================ FILE: assets/elevate.vbs ================================================ Set Shell = CreateObject("Shell.Application") Set WShell = WScript.CreateObject("WScript.Shell") Set ProcEnv = WShell.Environment("PROCESS") cmd = ProcEnv("CMD") app = ProcEnv("APP") args= Right(cmd,(Len(cmd)-Len(app))) If (WScript.Arguments.Count >= 1) Then Shell.ShellExecute app, args, "", "runas", 0 Else WScript.Quit End If ================================================ FILE: assets/install.cmd ================================================ @echo off set /P NVM_PATH="Enter the absolute path where the nvm-windows zip file is extracted/copied to: " set NVM_HOME=%NVM_PATH% set NVM_SYMLINK=C:\Program Files\nodejs setx /M NVM_HOME "%NVM_HOME%" setx /M NVM_SYMLINK "%NVM_SYMLINK%" echo PATH=%PATH% > %NVM_HOME%\PATH.txt for /f "skip=2 tokens=2,*" %%A in ('reg query "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" /v Path 2^>nul') do ( setx /M PATH "%%B;%%NVM_HOME%%;%%NVM_SYMLINK%%" ) if exist "%SYSTEMDRIVE%\Program Files (x86)\" ( set SYS_ARCH=64 ) else ( set SYS_ARCH=32 ) (echo root: %NVM_HOME% && echo path: %NVM_SYMLINK% && echo arch: %SYS_ARCH% && echo proxy: none) > %NVM_HOME%\settings.txt notepad %NVM_HOME%\settings.txt @echo on ================================================ FILE: assets/run.cmd ================================================ @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" ================================================ FILE: assets/setuserenv.vbs ================================================ Set WSHShell = CreateObject("WScript.Shell") userpath = WSHShell.RegRead("HKCU\Environment\Path") If InStr(userpath, "%NVM_HOME%") = False Then userpath = userpath & ";%NVM_HOME%;" End If If InStr(userpath, "%NVM_SYMLINK%") = False Then userpath = userpath & ";%NVM_SYMLINK%;" End If userpath = Replace(userpath, ";;", ";") WSHShell.RegWrite "HKCU\Environment\Path", userpath ================================================ FILE: assets/unsetuserenv.vbs ================================================ Set WSHShell = CreateObject("WScript.Shell") userpath = WSHShell.RegRead("HKCU\Environment\Path") userpath = Replace(userpath, "%NVM_HOME%", "") userpath = Replace(userpath, "%NVM_SYMLINK%", "") userpath = Replace(userpath, ";;", ";") WSHShell.RegWrite "HKCU\Environment\Path", userpath ================================================ FILE: build.js ================================================ // This is a deno helper script to locally build the installer using Inno Setup // Yeah, I know it's not Node, but we need to compile this and Node SEAs on Win32 are a PITA. const content = await Deno.readTextFile('./nvm.iss') const data = JSON.parse(await Deno.readTextFile('./src/manifest.json')) const {version} = data const output = content.replaceAll('{{VERSION}}', version) await Deno.writeTextFile('./.tmp.iss', output) console.log('Viewing /.tmp.iss') output.split("\n").forEach((line, num) => { let n = `${num+1}` while (n.length < 3) { n = ' ' + n } console.log(`${n} | ${line}`) }) const command = await new Deno.Command('.\\assets\\buildtools\\iscc.exe', { args: ['.\\.tmp.iss'], stdout: 'piped', stderr: 'piped', }) const process = command.spawn(); // Stream stdout (async () => { const decoder = new TextDecoder(); for await (const chunk of process.stdout) { console.log(decoder.decode(chunk)); } })(); // Stream stderr (async () => { const decoder = new TextDecoder(); for await (const chunk of process.stderr) { console.error(decoder.decode(chunk)); } })(); // Wait for completion const status = await process.status; Deno.remove('.\\.tmp.iss'); if (!status.success) { Deno.exit(status.code); } ================================================ FILE: examples/settings.txt ================================================ root: C:\Users\Corey\AppData\Roaming\nvm path: C:\Program Files\nodejs arch: 64 proxy: none ================================================ FILE: nvm.iss ================================================ #define MyAppName "NVM for Windows" #define MyAppShortName "nvm" #define MyAppLCShortName "nvm" #define MyAppVersion "{{VERSION}}" #define MyAppPublisher "Author Software Inc." #define MyAppURL "https://github.com/coreybutler/nvm-windows" #define MyAppExeName "nvm.exe" #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. PrivilegesRequired=admin SignTool=signtool SignedUninstaller=yes AppId={#MyAppId} AppName={#MyAppName} AppVersion={#MyAppVersion} AppCopyright=Copyright (C) 2018-{code:GetCurrentYear} Author Software Inc., Ecor Ventures LLC, Corey Butler, and contributors. AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={localappdata}\{#MyAppShortName} DisableDirPage=no DefaultGroupName={#MyAppName} AllowNoIcons=yes LicenseFile={#ProjectRoot}\LICENSE OutputDir={#ProjectRoot}\dist\{#MyAppVersion} OutputBaseFilename={#MyAppLCShortName}-setup SetupIconFile={#ProjectRoot}\{#MyIcon} Compression=lzma SolidCompression=yes ChangesEnvironment=yes DisableProgramGroupPage=yes ArchitecturesInstallIn64BitMode=x64 UninstallDisplayIcon={app}\{#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.js version manager for Windows VersionInfoProductName={#MyAppShortName} VersionInfoProductTextVersion={#MyAppVersion} [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 [Files] Source: "{#ProjectRoot}\bin\*"; DestDir: "{app}"; BeforeInstall: PreInstall; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "{#ProjectRoot}\bin\install.cmd" [Icons] Name: "{group}\{#MyAppShortName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{#MyIcon}" Name: "{group}\Uninstall {#MyAppShortName}"; Filename: "{uninstallexe}" [Registry] ; Register the URL protocol 'nvm' 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 SymlinkPage: TInputDirWizardPage; NotificationOptionPage: TInputOptionWizardPage; EmailPage: TWizardPage; EmailEdit: TEdit; EmailLabel: TLabel; PreText: TLabel; function GetCurrentYear(Param: String): String; begin result := GetDateTimeString('yyyy', '-', ':'); end; function IsDirEmpty(dir: string): Boolean; var FindRec: TFindRec; ct: Integer; begin ct := 0; if FindFirst(ExpandConstant(dir + '\*'), FindRec) then try repeat if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then ct := ct+1; until not FindNext(FindRec); finally FindClose(FindRec); Result := ct = 0; end; end; //function getInstalledVersions(dir: string): var nodeInUse: string; procedure TakeControl(np: string; nv: string); var path: string; begin // Move the existing node.js installation directory to the nvm root & update the path RenameFile(np,ExpandConstant('{app}')+'\'+nv); RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); StringChangeEx(path,np+'\','',True); StringChangeEx(path,np,'',True); StringChangeEx(path,np+';;',';',True); RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); StringChangeEx(path,np+'\','',True); StringChangeEx(path,np,'',True); StringChangeEx(path,np+';;',';',True); RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); nodeInUse := ExpandConstant('{app}')+'\'+nv; end; function Ansi2String(AString:AnsiString):String; var i : Integer; iChar : Integer; outString : String; begin outString :=''; for i := 1 to Length(AString) do begin iChar := Ord(AString[i]); //get int value outString := outString + Chr(iChar); end; Result := outString; end; procedure PreInstall(); var TmpResultFile, TmpJS, NodeVersion, NodePath: string; stdout: Ansistring; ResultCode: integer; msg1, msg2, msg3, dir1: Boolean; begin // Create a file to check for Node.JS TmpJS := ExpandConstant('{tmp}') + '\nvm_check.js'; SaveStringToFile(TmpJS, 'console.log(require("path").dirname(process.execPath));', False); // Execute the node file and save the output temporarily TmpResultFile := ExpandConstant('{tmp}') + '\nvm_node_check.txt'; Exec(ExpandConstant('{cmd}'), '/C node "'+TmpJS+'" > "' + TmpResultFile + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); DeleteFile(TmpJS) // Process the results LoadStringFromFile(TmpResultFile,stdout); NodePath := Trim(Ansi2String(stdout)); if DirExists(NodePath) then begin Exec(ExpandConstant('{cmd}'), '/C node -v > "' + TmpResultFile + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); LoadStringFromFile(TmpResultFile, stdout); NodeVersion := Trim(Ansi2String(stdout)); msg1 := SuppressibleMsgBox('Node '+NodeVersion+' is already installed. Do you want NVM to control this version?', mbConfirmation, MB_YESNO, IDYES) = IDNO; if msg1 then begin msg2 := SuppressibleMsgBox('NVM cannot run in parallel with an existing Node.js installation. Node.js must be uninstalled before NVM can be installed, or you must allow NVM to control the existing installation. Do you want NVM to control node '+NodeVersion+'?', mbConfirmation, MB_YESNO, IDYES) = IDYES; if msg2 then begin TakeControl(NodePath, NodeVersion); end; if not msg2 then begin DeleteFile(TmpResultFile); WizardForm.Close; end; end; if not msg1 then begin TakeControl(NodePath, NodeVersion); end; end; // Make sure the symlink directory doesn't exist if DirExists(SymlinkPage.Values[0]) then begin // If the directory is empty, just delete it since it will be recreated anyway. dir1 := IsDirEmpty(SymlinkPage.Values[0]); if dir1 then begin RemoveDir(SymlinkPage.Values[0]); end; if not dir1 then begin msg3 := SuppressibleMsgBox(SymlinkPage.Values[0]+' will be overwritten and all contents will be lost. Do you want to proceed?', mbConfirmation, MB_OKCANCEL, IDOK) = IDOK; if msg3 then begin RemoveDir(SymlinkPage.Values[0]); end; if not msg3 then begin //RaiseException('The symlink cannot be created due to a conflict with the existing directory at '+SymlinkPage.Values[0]); WizardForm.Close; end; end; end; end; function IsSymbolicLink(const Path: string): Boolean; var FindRec: TFindRec; begin Result := False; if FindFirst(Path, FindRec) then begin Result := (FindRec.Attributes and FILE_ATTRIBUTE_REPARSE_POINT) <> 0; FindClose(FindRec); end; end; procedure SymlinkPageChange(Sender: TObject); var NewPath: string; begin // Append '\nodejs' to the path if it is not already appended NewPath := AddBackslash(SymlinkPage.Values[0]) + 'nodejs'; if Copy(SymlinkPage.Values[0], Length(SymlinkPage.Values[0]) - Length('\nodejs') + 1, Length('\nodejs')) <> '\nodejs' then SymlinkPage.Values[0] := NewPath; // Check if the new path exists and is not a symbolic link if DirExists(NewPath) and not IsSymbolicLink(NewPath) then begin MsgBox('The directory "' + NewPath + '" already exists as a physical directory. Please choose a different location.', mbError, MB_OK); SymlinkPage.Values[0] := ''; end; end; procedure InitializeWizard; begin SymlinkPage := CreateInputDirPage(wpSelectDir, 'Active Version Location', 'The active version of Node.js will always be available at this location.', 'Select the folder in which Setup should create the symlink, then click Next.', False, ''); SymlinkPage.Add('This directory will automatically be added to your system path.'); SymlinkPage.Values[0] := ExpandConstant('C:\nvm4w\nodejs'); // Assign the OnChange event handler SymlinkPage.Edits[0].OnChange := @SymlinkPageChange; // Notification option page (after the Symlink page) NotificationOptionPage := CreateInputOptionPage( SymlinkPage.ID, // Ensures the Notification page appears right after the Symlink page 'Desktop Notifications (PREVIEW)', 'NVM for Windows supports the basic (free) edition of Author Notifications.', 'Select the events you wish to be notified of. Your choices can be modified at any time in the future.', FALSE, FALSE); // Pre-checked checkbox NotificationOptionPage.AddEx('Node.js LTS releases (Long-Term Support/Stable)', 0, FALSE); NotificationOptionPage.AddEx('Node.js Current releases (Latest/Testing)', 0, FALSE); NotificationOptionPage.AddEx('NVM For Windows releases', 0, FALSE); NotificationOptionPage.AddEx('Author updates and releases (upcoming NVM for Windows successor)', 0, FALSE); NotificationOptionPage.Values[0] := TRUE; NotificationOptionPage.Values[1] := TRUE; NotificationOptionPage.Values[2] := TRUE; NotificationOptionPage.Values[3] := TRUE; // Email Input Page EmailPage := CreateCustomPage( NotificationOptionPage.ID, 'Author Progress Email Signup', 'Get details about Author development milestones in your inbox.'); // Add introductory text above the input field PreText := TLabel.Create(WizardForm); PreText.Parent := EmailPage.Surface; PreText.Caption := 'Author is the upcoming successor to NVM for Windows. Provide your email address to be informed of development milestones, release timelines, and enterprise capabilities. ' + 'Leave it blank if you do not wish to receive notifications.'; PreText.Left := 10; PreText.Top := 10; PreText.Width := 600; // Adjust width to fit the text PreText.WordWrap := True; // Ensures the text wraps if it exceeds the width // Add a label for the email input field EmailLabel := TLabel.Create(WizardForm); EmailLabel.Parent := EmailPage.Surface; EmailLabel.Caption := 'Email Address: (Optional)'; EmailLabel.Font.Style := [fsBold]; EmailLabel.Left := 10; // Position from the left EmailLabel.Top := 80; // Position from the top // Add an email input field on the EmailPage EmailEdit := TEdit.Create(WizardForm); EmailEdit.Parent := EmailPage.Surface; EmailEdit.Left := 10; // Align with the label EmailEdit.Top := 110; // Position just below the label EmailEdit.Width := 610; 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; begin Result := True; // Allow navigation by default if CurPageID = SymlinkPage.ID then begin // Check if the directory is empty if DirExists(SymlinkPage.Values[0]) then begin if IsDirEmpty(SymlinkPage.Values[0]) then begin // If the directory is empty, just delete it since it will be recreated anyway. RemoveDir(SymlinkPage.Values[0]); end else begin // Show a warning if the directory is not empty MsgBox('The selected directory is not empty. Please choose a different path.', mbError, MB_OK); Result := False; // Prevent navigation to the next page end; end; end; if CurPageID = EmailPage.ID then begin Email := Trim(EmailEdit.Text); // Remove leading/trailing spaces 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; // Handle the Notification page logic if CurPageID = NotificationOptionPage.ID then begin if NotificationOptionPage.Values[0] then begin Log('User opted to enable Node.js release notifications.'); // Add your logic for enabling notifications here end else begin Log('User opted out of Node.js release notifications.'); end; end; end; function InitializeUninstall(): Boolean; var path: string; nvm_symlink: string; begin SuppressibleMsgBox('Removing NVM for Windows will remove the nvm command and all versions of node.js, including global npm modules.', mbInformation, MB_OK, IDOK); // Remove the symlink RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'NVM_SYMLINK', nvm_symlink); RemoveDir(nvm_symlink); // Clean the registry RegDeleteValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'NVM_HOME') RegDeleteValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'NVM_SYMLINK') RegDeleteValue(HKEY_CURRENT_USER, 'Environment', 'NVM_HOME') RegDeleteValue(HKEY_CURRENT_USER, 'Environment', 'NVM_SYMLINK') RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); StringChangeEx(path,'%NVM_HOME%','',True); StringChangeEx(path,'%NVM_SYMLINK%','',True); StringChangeEx(path,';;',';',True); RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); StringChangeEx(path,'%NVM_HOME%','',True); StringChangeEx(path,'%NVM_SYMLINK%','',True); StringChangeEx(path,';;',';',True); RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); Result := True; end; // Generate the settings file based on user input & update registry procedure CurStepChanged(CurStep: TSetupStep); var path: string; rootDir: string; begin if CurStep = ssInstall then begin // Ensure the root directory exists rootDir := ExtractFileDir(SymlinkPage.Values[0]); if not DirExists(rootDir) then begin if not CreateDir(rootDir) then begin MsgBox('Failed to create root directory: ' + rootDir, mbError, MB_OK); WizardForm.Close; end; end; end; if CurStep = ssPostInstall then begin SaveStringToFile(ExpandConstant('{app}\settings.txt'), 'root: ' + ExpandConstant('{app}') + #13#10 + 'path: ' + SymlinkPage.Values[0] + #13#10, False); // Add Registry settings RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'NVM_HOME', ExpandConstant('{app}')); RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'NVM_SYMLINK', SymlinkPage.Values[0]); RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'NVM_HOME', ExpandConstant('{app}')); RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'NVM_SYMLINK', SymlinkPage.Values[0]); RegWriteStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppId}_is1', 'DisplayVersion', '{#MyAppVersion}'); // Update system and user PATH if needed RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); if Pos('%NVM_HOME%',path) = 0 then begin path := path+';%NVM_HOME%'; StringChangeEx(path,';;',';',True); RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); end; if Pos('%NVM_SYMLINK%',path) = 0 then begin path := path+';%NVM_SYMLINK%'; StringChangeEx(path,';;',';',True); RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', path); end; RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); if Pos('%NVM_HOME%',path) = 0 then begin path := path+';%NVM_HOME%'; StringChangeEx(path,';;',';',True); RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); end; if Pos('%NVM_SYMLINK%',path) = 0 then begin path := path+';%NVM_SYMLINK%'; StringChangeEx(path,';;',';',True); RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path); end; end; end; function GetNotificationString(Param: String): String; begin Result := ' subscribe'; if NotificationOptionPage.Values[0] then begin Result := Result + ' --lts'; end; if NotificationOptionPage.Values[1] then begin Result := Result + ' --current'; end; if NotificationOptionPage.Values[2] then begin Result := Result + ' --nvm4w'; end; if NotificationOptionPage.Values[3] then begin Result := Result + ' --author'; end; Result := Trim(Result); end; function getSymLink(o: string): string; begin Result := SymlinkPage.Values[0]; end; function getCurrentVersion(o: string): string; begin Result := nodeInUse; end; function isNodeAlreadyInUse(): boolean; 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: "{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 refreshenv; cls; Write-Host 'Welcome to NVM for Windows v{{VERSION}}'"; Description: "Open with Powershell"; Flags: postinstall skipifsilent; [UninstallRun] Filename: "{app}\nvm.exe"; Parameters: "unsubscribe --lts --current --nvm4w --author"; Flags: runhidden; RunOnceId: "UnregisterNVMForWindows"; Filename: "{app}\nvm.exe"; Parameters: "off"; Flags: runhidden; RunOnceId: "RemoveNVMForWindowsSymlink"; Filename: "{cmd}"; Parameters: "/C rmdir /S /Q ""{code:getSymLink}"""; Flags: runhidden; RunOnceId: "RemoveSymlink"; [UninstallDelete] Type: filesandordirs; Name: "{app}"; Type: filesandordirs; Name: "{localappdata}\.nvm"; ================================================ FILE: src/arch/arch.go ================================================ package arch import ( //"regexp" "os" //"os/exec" "strings" //"fmt" "encoding/hex" ) func SearchBytesInFile( path string, match string, limit int) bool { // Transform to byte array the string toMatch, err := hex.DecodeString(match); if (err != nil) { return false; } // Opening the file and checking if there is an error file, err := os.Open(path) if err != nil { return false; } // Close file upon return defer file.Close() // Allocate 1 byte array to perform the match bit := make([]byte, 1); j := 0 for i := 0; i < limit; i++ { file.Read(bit); if bit[0] != toMatch[j] { j = 0; } if bit[0] == toMatch[j] { j++; if (j >= len(toMatch)) { file.Close(); return true; } } } file.Close(); return false; } func Bit(path string) string { isarm64 := SearchBytesInFile(path, "5045000064AA", 400) is64 := SearchBytesInFile(path, "504500006486", 400); is32 := SearchBytesInFile(path, "504500004C", 400); if isarm64 { return "arm64"; } else if is64 { return "64"; } else if is32 { return "32"; } return "?"; } func Validate(str string) (string){ if str == "" { str = strings.ToLower(os.Getenv("PROCESSOR_ARCHITECTURE")) } if strings.Contains(str, "arm64") { return "arm64" } if strings.Contains(str, "64") { return "64" } return "32" } ================================================ FILE: src/author/bridge.go ================================================ 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 { if !(len(args) == 1 && args[0] == "version") { 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) } } ================================================ FILE: src/encoding/encoding.go ================================================ package encoding import ( "strings" "unicode/utf8" "github.com/saintfish/chardet" ) func DetectCharset(content []byte) (string, error) { detector := chardet.NewTextDetector() result, err := detector.DetectBest(content) if err != nil { return "", err } return strings.ToUpper(result.Charset), nil } func ToUTF8(content string) []byte { b := make([]byte, len(content)) i := 0 for _, r := range content { i += utf8.EncodeRune(b[i:], r) } return b[:i] } // func ToUTF8(content []byte, ignoreInvalidITF8Chars ...bool) (string, error) { // ignore := false // if len(ignoreInvalidITF8Chars) > 0 { // ignore = ignoreInvalidITF8Chars[0] // } // cs, err := DetectCharset(content) // if err != nil { // if !ignore { // return "", err // } // cs = "UTF-8" // } // bs := string(content) // if ignore { // if !utf8.ValidString(bs) { // v := make([]rune, 0, len(bs)) // for i, r := range bs { // if r == utf8.RuneError { // _, size := utf8.DecodeRuneInString(bs[i:]) // if size == 1 { // continue // } // } // v = append(v, r) // } // bs = string(v) // } // } // if cs == "UTF-8" { // return bs, nil // } // converter, err := iconv.NewConverter(cs, "UTF-8") // if err != nil { // err = errors.New("Failed to convert " + cs + " to UTF-8: " + err.Error()) // return bs, err // } // return converter.ConvertString(bs) // } ================================================ FILE: src/file/file.go ================================================ package file import ( "archive/zip" "bufio" "io" "log" "os" "path/filepath" "strings" ) // Function courtesy http://stackoverflow.com/users/1129149/swtdrgn func Unzip(src, dest string) error { r, err := zip.OpenReader(src) if err != nil { return err } defer r.Close() for _, f := range r.File { if !strings.Contains(f.Name, "..") { rc, err := f.Open() if err != nil { return err } defer rc.Close() fpath := filepath.Join(dest, f.Name) if f.FileInfo().IsDir() { os.MkdirAll(fpath, f.Mode()) } else { var fdir string if lastIndex := strings.LastIndex(fpath, string(os.PathSeparator)); lastIndex > -1 { fdir = fpath[:lastIndex] } err = os.MkdirAll(fdir, f.Mode()) if err != nil { log.Fatal(err) return err } f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return err } defer f.Close() _, err = io.Copy(f, rc) if err != nil { return err } } } else { log.Printf("failed to extract file: %s (cannot validate)\n", f.Name) } } return nil } func ReadLines(path string) ([]string, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } return lines, scanner.Err() } func Exists(filename string) bool { _, err := os.Stat(filename) return err == nil } ================================================ FILE: src/go.mod ================================================ module nvm go 1.18 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/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.25.0 ) require ( 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 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 ) ================================================ FILE: src/go.sum ================================================ 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/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/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/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.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/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= ================================================ FILE: src/manifest.json ================================================ { "name": "nvm", "version": "1.2.2", "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" } } } } ================================================ FILE: src/node/node.go ================================================ package node import ( "encoding/json" "fmt" "io/ioutil" "nvm/arch" "nvm/file" "nvm/web" "os" "os/exec" "regexp" "strings" // "../semver" "github.com/blang/semver" ) /** * Returns version, architecture */ func GetCurrentVersion() (string, string) { cmd := exec.Command("node", "-v") str, err := cmd.Output() if err == nil { v := strings.Trim(regexp.MustCompile("-.*$").ReplaceAllString(regexp.MustCompile("v").ReplaceAllString(strings.Trim(string(str), " \n\r"), ""), ""), " \n\r") cmd := exec.Command("node", "-p", "console.log(process.execPath)") str, _ := cmd.Output() file := strings.Trim(regexp.MustCompile("undefined").ReplaceAllString(string(str), ""), " \n\r") bit := arch.Bit(file) if bit == "?" { cmd := exec.Command("node", "-e", "console.log(process.arch)") str, err := cmd.Output() if err == nil { if string(str) == "x64" { bit = "64" } else if string(str) == "arm64" { bit = "arm64" } else { bit = "32" } } else { return v, "Unknown" } } return v, bit } return "Unknown", "" } func IsVersionInstalled(root string, version string, cpu string) bool { e32 := file.Exists(root + "\\v" + version + "\\node32.exe") e64 := file.Exists(root + "\\v" + version + "\\node64.exe") used := file.Exists(root + "\\v" + version + "\\node.exe") if cpu == "all" { return ((e32 || e64) && used) || e32 && e64 } if file.Exists(root + "\\v" + version + "\\node" + cpu + ".exe") { return true } if ((e32 || e64) && used) || (e32 && e64) { return true } if !e32 && !e64 && used && arch.Validate(cpu) == arch.Bit(root+"\\v"+version+"\\node.exe") { return true } if cpu == "32" { return e32 } if cpu == "64" { return e64 } return false } func IsVersionAvailable(v string) bool { // Check the service to make sure the version is available avail, _, _, _, _, _ := GetAvailable() for _, b := range avail { if b == v { return true } } return false } func reverseStringArray(str []string) []string { for i := 0; i < len(str)/2; i++ { j := len(str) - i - 1 str[i], str[j] = str[j], str[i] } return str } func GetInstalled(root string) []string { list := make([]semver.Version, 0) files, _ := ioutil.ReadDir(root) for i := len(files) - 1; i >= 0; i-- { if files[i].IsDir() || (files[i].Mode()&os.ModeSymlink == os.ModeSymlink) { isnode, _ := regexp.MatchString("v", files[i].Name()) if isnode { currentVersionString := strings.Replace(files[i].Name(), "v", "", 1) currentVersion, _ := semver.Make(currentVersionString) list = append(list, currentVersion) } } } semver.Sort(list) loggableList := make([]string, 0) for _, version := range list { loggableList = append(loggableList, "v"+version.String()) } loggableList = reverseStringArray(loggableList) return loggableList } // Sorting type BySemanticVersion []string func (s BySemanticVersion) Len() int { return len(s) } func (s BySemanticVersion) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s BySemanticVersion) Less(i, j int) bool { v1, _ := semver.Make(s[i]) v2, _ := semver.Make(s[j]) return v1.GTE(v2) } // Identifies a version as "LTS" func isLTS(element map[string]interface{}) bool { switch datatype := element["lts"].(type) { case bool: return datatype case string: return true } return false } // Identifies a version as "current" func isCurrent(element map[string]interface{}) bool { if isLTS(element) { return false } version, _ := semver.Make(element["version"].(string)[1:]) benchmark, _ := semver.Make("1.0.0") if version.LT(benchmark) { return false } return true // return version.Major%2 == 1 } // Identifies a stable old version. func isStable(element map[string]interface{}) bool { if isCurrent(element) { return false } version, _ := semver.Make(element["version"].(string)[1:]) if version.Major != 0 { return false } return version.Minor%2 == 0 } // Identifies an unstable old version. func isUnstable(element map[string]interface{}) bool { if isStable(element) { return false } version, _ := semver.Make(element["version"].(string)[1:]) if version.Major != 0 { return false } return version.Minor%2 != 0 } // Retrieve the remotely available versions func GetAvailable() ([]string, []string, []string, []string, []string, map[string]string) { all := make([]string, 0) lts := make([]string, 0) current := make([]string, 0) stable := make([]string, 0) unstable := make([]string, 0) npm := make(map[string]string) url := web.GetFullNodeUrl("index.json") // Check the service to make sure the version is available text, err := web.GetRemoteTextFile(url) if err != nil { fmt.Println(err) os.Exit(1) } if len(text) == 0 { fmt.Println("Error retrieving version list: \"" + url + "\" returned blank results. This can happen when the remote file is being updated. Please try again in a few minutes.") os.Exit(0) } // Parse var data = make([]map[string]interface{}, 0) err = json.Unmarshal([]byte(text), &data) if err != nil { fmt.Printf("Error retrieving versions from \"%s\": %v", url, err.Error()) os.Exit(1) } for _, element := range data { var version = element["version"].(string)[1:] all = append(all, version) if val, ok := element["npm"].(string); ok { npm[version] = val } if isLTS(element) { lts = append(lts, version) } else if isCurrent(element) { current = append(current, version) } else if isStable(element) { stable = append(stable, version) } else if isUnstable(element) { unstable = append(unstable, version) } } return all, lts, current, stable, unstable, npm } ================================================ FILE: src/nvm.go ================================================ package main import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/url" "os" "os/exec" "os/signal" "os/user" "path/filepath" "regexp" "strconv" "strings" "sync" "syscall" "time" "unsafe" "nvm/arch" "nvm/author" "nvm/encoding" "nvm/file" "nvm/node" "nvm/upgrade" "nvm/utility" "nvm/web" "github.com/blang/semver" // "github.com/fatih/color" "github.com/coreybutler/go-where" "github.com/ncruces/zenity" "github.com/olekukonko/tablewriter" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) // Replaced at build time var NvmVersion = "" type Environment struct { settings string root string symlink string arch string node_mirror string npm_mirror string proxy string originalpath string originalversion string verifyssl bool } var home = filepath.Clean(os.Getenv("NVM_HOME") + "\\settings.txt") var symlink = filepath.Clean(os.Getenv("NVM_SYMLINK")) var env = &Environment{ settings: home, root: "", symlink: symlink, arch: strings.ToLower(os.Getenv("PROCESSOR_ARCHITECTURE")), node_mirror: "", npm_mirror: "", proxy: "none", originalpath: "", originalversion: "", verifyssl: true, } func writeToErrorLog(i interface{}, abort ...bool) { exe, _ := os.Executable() file, err := os.OpenFile(filepath.Join(filepath.Dir(exe), "error.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm) if err != nil { panic(err) } defer file.Close() msg := fmt.Sprintf("%v\n", i) if _, ferr := file.WriteString(msg); ferr != nil { panic(ferr) } if len(abort) > 0 && abort[0] { fmt.Println(msg) os.Exit(1) } } 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 notify(data Notification) { data.AppID = "NVM for Windows" content, _ := json.Marshal(data) go author.Bridge("notify", string(content)) } func init() { if len(os.Args) > 1 { if strings.HasPrefix(os.Args[1], "nvm://") { // Decode the URL uri, _ := url.Parse(strings.ReplaceAll(os.Args[1], "%26", "&")) action := strings.TrimSpace(uri.Query().Get("action")) switch strings.ToLower(action) { case "install": version := strings.Replace(uri.Query().Get("version"), "v", "", 1) os.Args[1] = "install" if len(os.Args) < 3 { os.Args = append(os.Args, "") } os.Args[2] = version if len(os.Args) < 4 { os.Args = append(os.Args, "") } os.Args[3] = "--show-progress-ui" os.Args = append(os.Args, "--insecure") case "use": version := strings.Replace(uri.Query().Get("version"), "v", "", 1) os.Args[1] = "use" if len(os.Args) < 3 { os.Args = append(os.Args, "") } os.Args[2] = version if len(os.Args) < 4 { os.Args = append(os.Args, "") } os.Args[3] = "--notify" case "upgrade": os.Args[1] = "upgrade" if len(os.Args) < 3 { os.Args = append(os.Args, "") } os.Args[2] = "--show-progress-ui" case "upgrade_notify": notify(Notification{ Title: "Upgrade Complete", Message: fmt.Sprintf("Now running v%v.", NvmVersion), Icon: "success", Actions: []Action{ {Type: "protocol", Label: "Release Notes", URI: fmt.Sprintf("https://github.com/coreybutler/nvm-windows/releases/tag/%v", NvmVersion)}, }, }) time.Sleep(300 * time.Millisecond) os.Exit(0) default: writeToErrorLog(fmt.Sprintf("%s command not recognized", action), true) } } } // Turn on debugging output for _, arg := range os.Args[1:] { if strings.ToLower(strings.ReplaceAll(arg, "-", "")) == "verbose" { utility.EnableDebugLogs() break } } } func main() { utility.DebugLogf("command: %v", strings.Join(os.Args, " ")) args := os.Args detail := "" procarch := arch.Validate(env.arch) // Capture any additional arguments if len(args) > 2 { detail = args[2] } if len(args) > 3 { if args[3] == "32" || args[3] == "arm64" || args[3] == "64" { procarch = args[3] } } if len(args) < 2 { help() return } utility.DebugLogf("arch: %v", procarch) if args[1] != "version" && args[1] != "--version" && args[1] != "v" && args[1] != "-v" && args[1] != "--v" { setup() } // Run the appropriate method switch args[1] { case "i": fallthrough case "install": install(detail, procarch) case "rm": fallthrough case "uninstall": uninstall(detail) case "reinstall": reinstall(detail, procarch) case "u": fallthrough case "use": use(detail, procarch) case "ls": fallthrough case "list": list(detail) case "on": enable() case "off": disable() case "root": if len(args) == 3 { updateRootDir(args[2]) } else { fmt.Println("\nCurrent Root: " + env.root) } case "v": fallthrough case "--version": fallthrough case "-version": fallthrough case "--v": fallthrough case "-v": fallthrough case "version": fmt.Println(NvmVersion) case "arch": if strings.Trim(detail, " \r\n") != "" { detail = strings.Trim(detail, " \r\n") if detail != "32" && detail != "64" && detail != "arm64" { fmt.Println("\"" + detail + "\" is an invalid architecture. Use 32, 64, or arm64.") return } env.arch = detail saveSettings() fmt.Println("Default architecture set to " + detail + "-bit.") return } _, a := node.GetCurrentVersion() fmt.Println("System Default: " + env.arch + "-bit.") fmt.Println("Currently Configured: " + a + "-bit.") case "proxy": if detail == "" { fmt.Println("Current proxy: " + env.proxy) } else { env.proxy = detail saveSettings() } case "current": inuse, _ := node.GetCurrentVersion() v, _ := semver.Make(inuse) err := v.Validate() if err != nil { fmt.Println(inuse) } else if inuse == "Unknown" { fmt.Println("No current version. Run 'nvm use x.x.x' to set a version.") } else { fmt.Println("v" + inuse) } //case "update": update() case "node_mirror": setNodeMirror(detail) case "npm_mirror": setNpmMirror(detail) case "debug": checkLocalEnvironment() case "subscribe": fallthrough case "unsubscribe": author.Bridge(args[1:]...) case "author": author.Bridge(args[2:]...) case "upgrade": upgrade.Run(NvmVersion) default: fmt.Printf(`"%s" is not a valid command.`+"\n", args[1]) help() } } // =============================================================== // BEGIN | CLI functions // =============================================================== func setNodeMirror(uri string) { env.node_mirror = uri saveSettings() } func setNpmMirror(uri string) { env.npm_mirror = uri saveSettings() } func getVersion(version string, cpuarch string, localInstallsOnly ...bool) (string, string, error) { requestedVersion := version cpuarch = strings.ToLower(cpuarch) if cpuarch != "" { if cpuarch != "32" && cpuarch != "64" && cpuarch != "arm64" && cpuarch != "all" { return version, cpuarch, errors.New("\"" + cpuarch + "\" is not a valid CPU architecture. Must be 32, 64, or arm64.") } } else { cpuarch = env.arch } if cpuarch != "all" { cpuarch = arch.Validate(cpuarch) } if version == "" { return "", cpuarch, errors.New("A version argument is required but missing.") } // If user specifies "latest" version, find out what version is if version == "latest" || version == "node" { version = getLatest() fmt.Println(version) } if version == "lts" { version = getLTS() } if version == "newest" { installed := node.GetInstalled(env.root) if len(installed) == 0 { return version, "", errors.New("No versions of node.js found. Try installing the latest by typing nvm install latest.") } version = installed[0] } if version == "32" || version == "64" || version == "arm64" { cpuarch = version v, _ := node.GetCurrentVersion() version = v } version = versionNumberFrom(version) v, err := semver.Make(version) if err == nil { err = v.Validate() } if err == nil { // if the user specifies only the major/minor version, identify the latest // version applicable to what was provided. sv := strings.Split(version, ".") if len(sv) < 3 { version = findLatestSubVersion(version) } else { version = cleanVersion(version) } version = versionNumberFrom(version) } else if strings.Contains(err.Error(), "No Major.Minor.Patch") { latestLocalInstall := false if len(localInstallsOnly) > 0 { latestLocalInstall = localInstallsOnly[0] } version = findLatestSubVersion(version, latestLocalInstall) if len(version) == 0 { err = errors.New("Unrecognized version: \"" + requestedVersion + "\"") } } return version, cpuarch, err } type Status struct { Text string Err error Done bool Help bool } func rollback(version string) error { p := filepath.Join(env.root, "v"+version) _, err := os.Lstat(p) if err != nil { if !os.IsNotExist(err) { writeToErrorLog(err) return fmt.Errorf("Error rolling back node v%s installation: %v.", version, err) } } return nil } func install(version string, cpuarch string) { requestedVersion := version args := os.Args lastarg := args[len(args)-1] if lastarg == "--insecure" { env.verifyssl = false } if strings.HasPrefix(version, "--") { fmt.Println("\"--\" prefixes are unnecessary in NVM for Windows!") version = strings.ReplaceAll(version, "-", "") fmt.Printf("attempting to install \"%v\" instead...\n\n", version) time.Sleep(2 * time.Second) } var exitCode = 0 var status = make(chan Status) var cancel = make(chan bool) var show_progress bool = false var dlg zenity.ProgressDialog wg := &sync.WaitGroup{} wg.Add(1) go func() { defer func() { // Reset the SSL verification env.verifyssl = true // sleep for 1 second to give users a chance to see the completion notice before exiting if show_progress { time.Sleep(1 * time.Second) fmt.Println("exiting installer dialog") } wg.Done() }() for { select { case s := <-status: if s.Err != nil { exitCode = 1 if show_progress && dlg != nil { // Close progress dialog and send error notificaion notify(Notification{ Title: "Node.js Installation Error", Message: s.Err.Error(), Icon: "error", }) dlg.Text(fmt.Sprintf("error: %v", s.Err)) dlg.Close() // Cleanup err := rollback(version) if err != nil { notify(Notification{ Title: "Rollback Error", Message: err.Error(), Icon: "error", Actions: []Action{ {Type: "protocol", Label: "Open Explorer", URI: fmt.Sprintf("file://%s", filepath.ToSlash(filepath.Join(env.root)))}, }, }) } } else { fmt.Printf("error installing %s: %v\n", version, s.Err) if s.Help { fmt.Println(" ") help() } } return } if s.Done { if show_progress && dlg != nil { notify(Notification{ Title: fmt.Sprintf("Node.js v%s", version), Message: "Installation complete.", Icon: "node", Actions: []Action{ {Type: "protocol", Label: "Use", URI: fmt.Sprintf("nvm://launch?action=use%%26version=%s", version)}, {Type: "protocol", Label: "Changelog", URI: fmt.Sprintf("https://github.com/nodejs/node/releases/tag/v%s", version)}, }, }) dlg.Text("Installation complete.") time.Sleep(1 * time.Second) } else { fmt.Printf("Installation complete.\nIf you want to use this version, type:\n\nnvm use %s\n", version) } return } if show_progress && dlg != nil { dlg.Text(s.Text) } else { fmt.Println(s.Text) } case <-cancel: fmt.Printf("Node.js %s installation canceled by user\n", version) if show_progress { notify(Notification{ Title: fmt.Sprintf("Node.js v%s", version), Message: "Installation canceled by user", Icon: "error", Actions: []Action{ {Type: "protocol", Label: "Restart Installation", URI: fmt.Sprintf("nvm://launch?action=install%%26version=%s%%26use=false%%26=show=true", version)}, }, }) } err := rollback(version) if err != nil { if show_progress { notify(Notification{ Title: "Rollback Error", Message: err.Error(), Icon: "error", Actions: []Action{ {Type: "protocol", Label: "Open Explorer", URI: fmt.Sprintf("file://%s", filepath.ToSlash(filepath.Join(env.root)))}, }, }) } else { fmt.Printf("rollback error: %v\n", err) } } else { fmt.Println("Rollback complete.") } if cpuarch == "arm64" && !web.IsNodeArm64bitAvailable(version) { status <- Status{Err: fmt.Errorf("Node.js v%s is only available in 32-bit and 64-bit.", version)} } if !node.IsVersionInstalled(env.root, version, cpuarch) { if !node.IsVersionAvailable(version) { url := web.GetFullNodeUrl("index.json") status <- Status{Err: fmt.Errorf("Version %s is not available.\n\nThe complete list of available versions can be found at %s", version, url)} } } return } } }() // Wait for the prior subroutine to initialize before starting the next dependent thread time.Sleep(300 * time.Millisecond) go func() { v, a, err := getVersion(version, cpuarch) version = v cpuarch = a // 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 cancel <- true }() // Determine whether to show the progress dialog for _, arg := range os.Args { if arg == "--show-progress-ui" { show_progress = true exe, _ := os.Executable() winIco := filepath.Join(filepath.Dir(exe), "nvm.ico") ico := filepath.Join(filepath.Dir(exe), "nodejs.ico") var perr error dlg, perr = zenity.Progress( zenity.Title(fmt.Sprintf("Installing Node.js v%s", 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 { cancel <- true } return } } }() status <- Status{Text: "Validating version..."} break } } if err != nil { if strings.Contains(err.Error(), "No Major.Minor.Patch") { sv, sverr := semver.Make(version) if sverr == nil { sverr = sv.Validate() } if sverr != nil { version = findLatestSubVersion(version) if len(version) == 0 { sverr = errors.New("Unrecognized version: \"" + requestedVersion + "\"") } } err = sverr } if err != nil { status <- Status{Err: err, Help: true} return } } if err != nil { status <- Status{Err: fmt.Errorf(`"%s" is not a valid version.`+"\n"+`Please use a valid semantic version number, "lts", or "latest".`, requestedVersion)} return } if checkVersionExceedsLatest(version) { status <- Status{Err: fmt.Errorf("Node.js v%s is not yet released or is not available for download yet.", version)} return } if cpuarch == "64" && !web.IsNode64bitAvailable(version) { status <- Status{Err: fmt.Errorf("Node.js v%s is only available in 32-bit.", version)} return } // Check to see if the version is already installed if !node.IsVersionInstalled(env.root, version, cpuarch) { if !node.IsVersionAvailable(version) { url := web.GetFullNodeUrl("index.json") status <- Status{Err: fmt.Errorf("Version %s is not available.\n\nThe complete list of available versions can be found at %s", version, url)} return } // Make the output directories root, err := os.MkdirTemp("", "nvm-install-*") if err != nil { status <- Status{Err: err} } defer os.RemoveAll(root) os.MkdirAll(filepath.Join(root, "v"+version, "node_modules"), os.ModeDir) // os.MkdirAll(filepath.Join(env.root, "v"+version, "node_modules"), os.ModeDir) // Warn the user if they're attempting to install without verifying the remote SSL cert if !env.verifyssl { fmt.Println("\nWARNING: The remote SSL certificate will not be validated during the download process.\n") } // Download node if show_progress { status <- Status{Text: "Downloading & extracting..."} } append32 := node.IsVersionInstalled(env.root, version, "64") append64 := node.IsVersionInstalled(env.root, version, "32") if (cpuarch == "32" || cpuarch == "all") && !node.IsVersionInstalled(root, version, "32") { success := web.GetNodeJS(root, version, "32", append32) if !success { status <- Status{Err: fmt.Errorf("failed to download v%v 32-bit executable", version)} return } } if (cpuarch == "64" || cpuarch == "all") && !node.IsVersionInstalled(root, version, "64") { success := web.GetNodeJS(root, version, "64", append64) if !success { status <- Status{Err: fmt.Errorf("failed to download v%v 64-bit executable", version)} return } } if (cpuarch == "arm64" || cpuarch == "all") && !node.IsVersionInstalled(root, version, "arm64") { success := web.GetNodeJS(root, version, "arm64", append64) if !success { status <- Status{Err: fmt.Errorf("failed to download v%v arm 64-bit executable", version)} return } } if file.Exists(filepath.Join(root, "v"+version, "node_modules", "npm")) { utility.DebugLogf("move %v to %v", filepath.Join(root, "v"+version), filepath.Join(env.root, "v"+version)) if rnerr := utility.Rename(filepath.Join(root, "v"+version), filepath.Join(env.root, "v"+version)); rnerr != nil { status <- Status{Err: err} } utility.DebugFn(func() { utility.DebugLogf("env root: %v", env.root) cmd := exec.Command("cmd", "/C", "dir", filepath.Join(env.root, "v"+version)) out, err := cmd.CombinedOutput() if err != nil { utility.DebugLog(err.Error()) } else { utility.DebugLog(string(out)) } }) if show_progress { status <- Status{Text: "Configuring npm..."} time.Sleep(1 * time.Second) status <- Status{Done: true} } else { npmv := getNpmVersion(version) status <- Status{Text: fmt.Sprintf("npm v%s installed successfully.\n\nIf you want to use this version, type\n\nnvm use %s", npmv, version), Done: true} } } // If successful, add npm status <- Status{Text: "Downloading npm..."} npmv := getNpmVersion(version) success := web.GetNpm(root, getNpmVersion(version)) if success { status <- Status{Text: fmt.Sprintf("Installing npm v%s...", npmv)} // new temp directory under the nvm root tempDir, err := os.MkdirTemp("", "nvm-npm-*") if err != nil { status <- Status{Err: err} } defer os.RemoveAll(tempDir) // Extract npm to the temp directory err = file.Unzip(filepath.Join(tempDir, "npm-v"+npmv+".zip"), filepath.Join(tempDir, "nvm-npm")) if err != nil { status <- Status{Err: err} } // Copy the npm and npm.cmd files to the installation directory tempNpmBin := filepath.Join(tempDir, "nvm-npm", "cli-"+npmv, "bin") // Support npm < 6.2.0 if file.Exists(tempNpmBin) == false { tempNpmBin = filepath.Join(tempDir, "nvm-npm", "npm-"+npmv, "bin") } if file.Exists(tempNpmBin) == false { status <- Status{Err: fmt.Errorf("Failed to extract npm. Could not find %s", tempNpmBin), Done: true} return } // Standard npm support utility.Rename(filepath.Join(tempNpmBin, "npm"), filepath.Join(root, "v"+version, "npm")) utility.Rename(filepath.Join(tempNpmBin, "npm.cmd"), filepath.Join(root, "v"+version, "npm.cmd")) // npx support if _, err := os.Stat(filepath.Join(tempNpmBin, "npx")); err == nil { utility.Rename(filepath.Join(tempNpmBin, "npx"), filepath.Join(root, "v"+version, "npx")) utility.Rename(filepath.Join(tempNpmBin, "npx.cmd"), filepath.Join(root, "v"+version, "npx.cmd")) } npmSourcePath := filepath.Join(tempDir, "nvm-npm", "npm-"+npmv) if file.Exists(npmSourcePath) == false { npmSourcePath = filepath.Join(tempDir, "nvm-npm", "cli-"+npmv) } moveNpmErr := utility.Rename(npmSourcePath, filepath.Join(root, "v"+version, "node_modules", "npm")) if moveNpmErr != nil { // sometimes Windows can take some time to enable access to large amounts of files after unzip, use exponential backoff to wait until it is ready for _, i := range [5]int{1, 2, 4, 8, 16} { time.Sleep(time.Duration(i) * time.Second) moveNpmErr = utility.Rename(npmSourcePath, filepath.Join(root, "v"+version, "node_modules", "npm")) if moveNpmErr == nil { break } } } if err == nil && moveNpmErr == nil { err = utility.Rename(filepath.Join(root, "v"+version), filepath.Join(env.root, "v"+version)) if err != nil { status <- Status{Err: err} } if show_progress { status <- Status{Done: true} } else { status <- Status{Text: fmt.Sprintf("Installation complete. If you want to use this version, type\n\nnvm use %s", version), Done: true} } } else if moveNpmErr != nil { status <- Status{Err: fmt.Errorf("Unable to move directory %s to node_modules: %v", npmSourcePath, moveNpmErr), Done: true} } else { status <- Status{Err: fmt.Errorf("Failed to extract npm: %v", err), Done: true} } } else { err = utility.Rename(filepath.Join(root, "v"+version), filepath.Join(env.root, "v"+version)) if err != nil { status <- Status{Err: err} } npmurl := web.GetFullNpmUrl(version) if show_progress { // Send special error notification with link to npm release when it cannot be downloaded notify(Notification{ Title: "Download Failure (npm)", Message: fmt.Sprintf("Please download npm v%s manually and extract to %s\\v%s", version, env.root, version), Icon: "error", Actions: []Action{ {Type: "protocol", Label: "Manually Download", URI: npmurl}, }, }) status <- Status{Done: true} } else { status <- Status{Err: fmt.Errorf("Could not download npm for node v%s.\nPlease visit %s to download npm.\nIt should be extracted to %s\\v%s", version, npmurl, env.root, version), Done: true} } } } else { status <- Status{Text: "Version " + version + " is already installed.", Done: true} } }() // Wait for the process to complete before exiting wg.Wait() os.Exit(exitCode) } 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))) abortOnBadSymlink(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) { // 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") || node.IsVersionInstalled(env.root, version, "arm64") { fmt.Printf("Uninstalling node v" + version + "...") v, _ := node.GetCurrentVersion() if v == version { // _, err := runElevated(fmt.Sprintf(`"%s" cmd /C rmdir "%s"`, filepath.Join(env.root, "elevate.cmd"), filepath.Clean(env.symlink))) abortOnBadSymlink(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.Println("Error removing node v" + version) fmt.Println("Manually remove " + filepath.Join(env.root, "v"+version) + ".") } else { fmt.Printf(" done") } } else { fmt.Println("node v" + version + " is not installed. Type \"nvm list\" to see what is installed.") } return } func versionNumberFrom(version string) string { reg, _ := regexp.Compile("[^0-9]") if reg.MatchString(version[:1]) { if version[0:1] != "v" { url := web.GetFullNodeUrl("latest-" + version + "/SHASUMS256.txt") remoteContent, err := web.GetRemoteTextFile(url) if err != nil { fmt.Println(err) os.Exit(1) } content := strings.Split(remoteContent, "\n")[0] if strings.Contains(content, "node") { parts := strings.Split(content, "-") if len(parts) > 1 { if parts[1][0:1] == "v" { return parts[1][1:] } } } fmt.Printf("\"%v\" is not a valid version or known alias.\n", version) fmt.Println("\nAvailable aliases: latest, node (latest), lts\nNamed releases (boron, dubnium, etc) are also supported.") os.Exit(0) } } for reg.MatchString(version[:1]) { version = version[1:] } return version } func splitVersion(version string) map[string]int { parts := strings.Split(version, ".") var result = make([]int, 3) for i, item := range parts { v, _ := strconv.Atoi(item) result[i] = v } return map[string]int{ "major": result[0], "minor": result[1], "patch": result[2], } } func findLatestSubVersion(version string, localOnly ...bool) string { if len(localOnly) > 0 && localOnly[0] { installed := node.GetInstalled(env.root) result := "" for _, v := range installed { if strings.HasPrefix(v, "v"+version) { if result != "" { current, _ := semver.New(versionNumberFrom(result)) next, _ := semver.New(versionNumberFrom(v)) if current.LT(*next) { result = v } } else { result = v } } } if len(strings.TrimSpace(result)) > 0 { return versionNumberFrom(result) } } if len(strings.Split(version, ".")) == 2 { all, _, _, _, _, _ := node.GetAvailable() requested := splitVersion(version + ".0") for _, v := range all { available := splitVersion(v) if requested["major"] == available["major"] { if requested["minor"] == available["minor"] { if available["patch"] > requested["patch"] { requested["patch"] = available["patch"] } } if requested["minor"] > available["minor"] { break } } if requested["major"] > available["major"] { break } } return fmt.Sprintf("%v.%v.%v", requested["major"], requested["minor"], requested["patch"]) } url := web.GetFullNodeUrl("latest-v" + version + ".x" + "/SHASUMS256.txt") content, err := web.GetRemoteTextFile(url) if err != nil { if strings.Contains(err.Error(), "HTTP Status 404") { fmt.Printf("\"%s\" is not a valid version number (or partial version number).\n\nIf you are trying to install a version that was just announced within the last few minutes, it may not be available for download yet (try again in 15 minutes).\n", version) } else { fmt.Println(err) } os.Exit(1) } re := regexp.MustCompile("node-v(.+)+msi") reg := regexp.MustCompile("node-v|-[xa].+") latest := reg.ReplaceAllString(re.FindString(content), "") return latest } func accessDenied(err error) bool { fmt.Println(fmt.Sprintf("%v", err)) if strings.Contains(strings.ToLower(err.Error()), "access is denied") { fmt.Println("See https://bit.ly/nvm4w-help") return true } return false } func isSymlink(path string) (bool, error) { info, err := os.Lstat(path) if err != nil { return false, err } return info.Mode()&os.ModeSymlink != 0, nil } func use(version string, cpuarch string, reload ...bool) { version, cpuarch, err := getVersion(version, cpuarch, true) exitCode := 0 status := make(chan Status) wg := &sync.WaitGroup{} wg.Add(1) notifications := false if os.Args[len(os.Args)-1] == "--notify" { notifications = true } go func() { defer func() { if notifications { time.Sleep(1 * time.Second) } wg.Done() }() for { select { case s := <-status: if s.Err != nil { exitCode = 1 if notifications { // Close progress dialog and send error notificaion notify(Notification{ Title: "Node.js Activation Error", Message: fmt.Sprintf("nvm use %s failed because %v", version, s.Err), Icon: "error", }) } fmt.Printf("activation error: %v\n", s.Err) if s.Help { fmt.Println(" ") help() } return } if s.Done { if s.Err == nil { if notifications { notify(Notification{ Title: "Node.js Activated", Message: fmt.Sprintf("Your system is now configured to use v%s (%v-bit).", version, cpuarch), Icon: "success", Actions: []Action{ {Type: "protocol", Label: "View Changelog", URI: fmt.Sprintf("https://github.com/nodejs/node/releases/tag/v%s", version)}, }, }) } fmt.Printf("Now using node v%s (%v-bit)\n", version, cpuarch) } return } if len(strings.TrimSpace(s.Text)) > 0 { fmt.Println(s.Text) } } } }() time.Sleep(300 * time.Millisecond) go func() { if err != nil { if !strings.Contains(err.Error(), "No Major.Minor.Patch") { status <- Status{Err: err, Done: true} } } // Check if a change is needed curVersion, curCpuarch := node.GetCurrentVersion() if version == curVersion && cpuarch == curCpuarch { fmt.Println("node v" + version + " (" + cpuarch + "-bit) is already in use.") status <- Status{Done: true} return } // Make sure the version is installed. If not, warn. if !node.IsVersionInstalled(env.root, version, cpuarch) { err = fmt.Errorf("node v%s (%v-bit) is not installed.", version, cpuarch) if notifications { status <- Status{Err: err, Done: true} } if (cpuarch == "32" && node.IsVersionInstalled(env.root, version, "64")) || (cpuarch == "64" && node.IsVersionInstalled(env.root, version, "32")) { status <- Status{Err: fmt.Errorf("Did you mean node v%s (%v-bit)?\nIf so, type \"nvm use %s %v\" to use it.", version, cpuarch, version, cpuarch), Done: true} } status <- Status{Err: fmt.Errorf("Version not installed. Run \"nvm ls\" to see available versions."), Done: true} } // Remove symlink if it already exists sym, _ := os.Lstat(env.symlink) if sym != nil { err = validSymlink(env.symlink) if err != nil { status <- Status{Err: err, Done: true} } // _, 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 { if accessDenied(err) { status <- Status{Err: err, Done: true} } } } // Create new symlink var ok bool // ok, err = runElevated(fmt.Sprintf(`"%s" cmd /C mklink /D "%s" "%s"`, filepath.Join(env.root, "elevate.cmd"), filepath.Clean(env.symlink), filepath.Join(env.root, "v"+version))) ok, err = elevatedRun("mklink", "/D", filepath.Clean(env.symlink), filepath.Join(env.root, "v"+version)) if err != nil { if strings.Contains(err.Error(), "not have sufficient privilege") || strings.Contains(strings.ToLower(err.Error()), "access is denied") { ok, err = elevatedRun("mklink", "/D", filepath.Clean(env.symlink), filepath.Join(env.root, "v"+version)) if err != nil { ok = false status <- Status{Err: err, Done: true} // fmt.Println(fmt.Sprint(err)) // + ": " + _stderr.String()) } else { ok = true } } else if strings.Contains(err.Error(), "file already exists") { err = validSymlink(env.symlink) if err != nil { status <- Status{Err: err, Done: true} } ok, err = elevatedRun("rmdir", filepath.Clean(env.symlink)) // ok, err = runElevated(fmt.Sprintf(`"%s" cmd /C rmdir "%s"`, filepath.Join(env.root, "elevate.cmd"), filepath.Clean(env.symlink))) reloadable := true if len(reload) > 0 { reloadable = reload[0] } if err != nil { status <- Status{Err: err, Done: true} } else if reloadable { use(version, cpuarch, false) return } } else { status <- Status{Err: err, Done: true} } } if !ok { status <- Status{Err: fmt.Errorf("failed to elevate permissions to create symlink"), Done: true} } // Use the assigned CPU architecture cpuarch = arch.Validate(cpuarch) nodepath := filepath.Join(env.root, "v"+version, "node.exe") node32path := filepath.Join(env.root, "v"+version, "node32.exe") node64path := filepath.Join(env.root, "v"+version, "node64.exe") node32exists := file.Exists(node32path) node64exists := file.Exists(node64path) nodeexists := file.Exists(nodepath) if node32exists && cpuarch == "32" { // user wants 32, but node.exe is 64 if nodeexists { utility.Rename(nodepath, node64path) // node.exe -> node64.exe } utility.Rename(node32path, nodepath) // node32.exe -> node.exe } if node64exists && cpuarch == "64" { // user wants 64, but node.exe is 32 if nodeexists { utility.Rename(nodepath, node32path) // node.exe -> node32.exe } utility.Rename(node64path, nodepath) // node64.exe -> node.exe } status <- Status{Done: true} }() // Wait for the process to complete before exiting wg.Wait() os.Exit(exitCode) } func abortOnBadSymlink(symlinkpath string) { if err := validSymlink(symlinkpath); err != nil { fmt.Printf("%v\n", err) os.Exit(1) } } func validSymlink(symlinkpath string) error { symlinkpath = filepath.Clean(symlinkpath) // Prevent deletion if the symlink has been set to a physical directpry/file. // This isn't supposed to ever happen, but users have manually changed the settings.txt, // removing the physical file/directory unintentionally. // This is an anti-footgun. if symlink, err := isSymlink(symlinkpath); !symlink && err == nil { return fmt.Errorf("NVM_SYMLINK is set to a physical file/directory at %s\nPlease remove the location and try again, or select a different location for NVM_SYMLINK.\n", env.symlink) } return nil } func useArchitecture(a string) { if strings.ContainsAny("32", os.Getenv("PROCESSOR_ARCHITECTURE")) { fmt.Println("This computer only supports 32-bit processing.") return } if strings.Contains("arm64", strings.ToLower(os.Getenv("PROCESSOR_ARCHITECTURE"))) { fmt.Println("This computer only supports arm64-bit processing.") return } if a == "32" || a == "64" { env.arch = a saveSettings() fmt.Println("Set to " + a + "-bit mode") } else { fmt.Println("Cannot set architecture to " + a + ". Must be 32 or 64 are acceptable values.") } } func list(listtype string) { if listtype == "" { listtype = "installed" } if listtype != "installed" && listtype != "available" { fmt.Println("\nInvalid list option.\n\nPlease use on of the following\n - nvm list\n - nvm list installed\n - nvm list available") help() return } if listtype == "installed" { fmt.Println("") inuse, a := node.GetCurrentVersion() v := node.GetInstalled(env.root) for i := 0; i < len(v); i++ { version := v[i] isnode, _ := regexp.MatchString("v", version) str := "" if isnode { if "v"+inuse == version { str = str + " * " } else { str = str + " " } str = str + regexp.MustCompile("v").ReplaceAllString(version, "") if "v"+inuse == version { str = str + " (Currently using " + a + "-bit executable)" // str = ansi.Color(str,"green:black") } fmt.Printf(str + "\n") } } if len(v) == 0 { fmt.Println("No installations recognized.") } } else { _, lts, current, stable, unstable, _ := node.GetAvailable() releases := 20 data := make([][]string, releases, releases+5) for i := 0; i < releases; i++ { release := make([]string, 4, 6) release[0] = "" release[1] = "" release[2] = "" release[3] = "" if len(current) > i { if len(current[i]) > 0 { release[0] = current[i] } } if len(lts) > i { if len(lts[i]) > 0 { release[1] = lts[i] } } if len(stable) > i { if len(stable[i]) > 0 { release[2] = stable[i] } } if len(unstable) > i { if len(unstable[i]) > 0 { release[3] = unstable[i] } } data[i] = release } fmt.Println("") table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{" Current ", " LTS ", " Old Stable ", "Old Unstable"}) table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) table.SetAlignment(tablewriter.ALIGN_CENTER) table.SetCenterSeparator("|") table.AppendBulk(data) // Add Bulk Data table.Render() fmt.Println("\nThis is a partial list. For a complete list, visit https://nodejs.org/en/download/releases") } } func enable() { dir := "" files, _ := ioutil.ReadDir(env.root) for _, f := range files { if f.IsDir() { isnode, _ := regexp.MatchString("v", f.Name()) if isnode { dir = f.Name() } } } fmt.Println("nvm enabled") if dir != "" { use(strings.Trim(regexp.MustCompile("v").ReplaceAllString(dir, ""), " \n\r"), env.arch) } else { fmt.Println("No versions of node.js found. Try installing the latest by typing nvm install latest") } } func disable() { // ok, err := runElevated(fmt.Sprintf(`"%s" cmd /C rmdir "%s"`, filepath.Join(env.root, "elevate.cmd"), filepath.Clean(env.symlink))) abortOnBadSymlink(env.symlink) ok, err := elevatedRun("rmdir", filepath.Clean(env.symlink)) if !ok { return } if err != nil { fmt.Print(fmt.Sprint(err)) } fmt.Println("nvm disabled") } const ( VER_PLATFORM_WIN32s = 0 VER_PLATFORM_WIN32_WINDOWS = 1 VER_PLATFORM_WIN32_NT = 2 ) type OSVersionInfoEx struct { OSVersionInfoSize uint32 MajorVersion uint32 MinorVersion uint32 BuildNumber uint32 PlatformId uint32 CSDVersion [128]uint16 } func checkLocalEnvironment() { problems := make([]string, 0) // Check for PATH problems paths := strings.Split(os.Getenv("PATH"), ";") current := env.symlink if strings.HasSuffix(current, "/") || strings.HasSuffix(current, "\\") { current = current[:len(current)-1] } nvmsymlinkfound := false for _, path := range paths { if strings.HasSuffix(path, "/") || strings.HasSuffix(path, "\\") { path = path[:len(path)-1] } if strings.EqualFold(path, current) { nvmsymlinkfound = true break } if _, err := os.Stat(filepath.Join(path, "node.exe")); err == nil { problems = append(problems, "Another Node.js installation is blocking NVM4W installations from running. Please uninstall the conflicting version or update the PATH environment variable to assure \""+current+"\" precedes \""+path+"\".") break } else if !errors.Is(err, os.ErrNotExist) { fmt.Println("Error running environment check:\n" + err.Error()) } } // Check for developer mode devmode := "OFF" k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock`, registry.QUERY_VALUE) if err == nil { value, _, err := k.GetIntegerValue("AllowDevelopmentWithoutDevLicense") if err == nil { if value > 0 { devmode = "ON" } } else { devmode = "UNKNOWN" } } else { devmode = "UNKNOWN (user cannot read registry)" } defer k.Close() // Check for permission problems admin, elevated, err := getProcessPermissions() if err == nil { if !admin && !elevated { user, _ := user.Current() username := strings.Split(user.Username, "\\") fmt.Printf("%v is not using admin or elevated rights", username[len(username)-1]) if devmode == "ON" { fmt.Printf(", but windows developer mode is\nenabled. Most commands will still work unless %v lacks rights to\nmodify the %v symlink.\n", username[len(username)-1], current) } else { fmt.Println(".") } } else { if admin { fmt.Println("Running NVM for Windows with administrator privileges.") } else if elevated { fmt.Println("Running NVM for Windows with elevated permissions.") } } } else { fmt.Println(err) } kernel32 := syscall.NewLazyDLL("kernel32.dll") handle, _, err := kernel32.NewProc("GetStdHandle").Call(uintptr(0xfffffff5)) // get handle for console input if err != nil && err.Error() != "The operation completed successfully." { fmt.Printf("Error getting console handle: %v", err) } else { var mode uint32 result, _, _ := kernel32.NewProc("GetConsoleMode").Call(handle, uintptr(unsafe.Pointer(&mode))) if result != 0 { var title [256]uint16 _, _, err := kernel32.NewProc("GetConsoleTitleW").Call(uintptr(unsafe.Pointer(&title)), uintptr(len(title))) if err != nil && err.Error() != "The operation completed successfully." { fmt.Printf("Error getting console title: %v", err) } else { consoleTitle := syscall.UTF16ToString(title[:]) 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") && !strings.Contains(strings.ToLower(consoleTitle), "powershell.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)) } } } } // Get Windows details getVersionEx := kernel32.NewProc("GetVersionExW") versionInfo := OSVersionInfoEx{ OSVersionInfoSize: uint32(unsafe.Sizeof(OSVersionInfoEx{})), } ret, _, _ := getVersionEx.Call(uintptr(unsafe.Pointer(&versionInfo))) if ret == 0 { fmt.Println("Failed to retrieve version information.") } // 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) // SHELL in Linux // TERM in Windows // COMSPEC in Windows provides the terminal path // shell := os.Getenv("ConEmuANSI") // fmt.Printf("Shell: %v\n", shell) // Display developer mode status if !admin { // if devmode == "ON" { // devmode = color.GreenString(devmode) // } else if devmode == "OFF" { // devmode = color.YellowString(devmode) // } else { // devmode = color.YellowString(devmode) // } fmt.Printf("\n%v %v\n", "Windows Developer Mode:", devmode) } executable := os.Args[0] path, err := where.Find(filepath.Base(executable)) if err != nil { path = "UNKNOWN: " + err.Error() } out := "none\n(run \"nvm use \" to activate a version)\n" output, err := exec.Command(os.Getenv("NVM_SYMLINK")+"\\node.exe", "-v").Output() if err == nil { out = string(output) } v := node.GetInstalled(env.root) // Make sure author-nvm.exe is available and runs exe, _ := os.Executable() valueBytes, err := exec.Command(exe, "author", "version").Output() authorNvmVersion := "Not Detected" if err != nil { // fmt.Println("Error running author-nvm.exe: " + err.Error()) problems = append(problems, "The author-nvm.exe file is missing or not executable.") } else { authorNvmVersion = strings.TrimSpace(string(valueBytes)) } nvmhome := os.Getenv("NVM_HOME") 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 Author Bridge: %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, authorNvmVersion, 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.") } if home == symlink { problems = append(problems, "NVM_HOME and NVM_SYMLINK cannot be the same value ("+symlink+"). Change NVM_SYMLINK.") } fileInfo, err := os.Lstat(symlink) if err != nil { if os.IsNotExist(err) { fmt.Println("NVM_SYMLINK does not exist yet. This is auto-created when \"nvm use\" is run.") } else { problems = append(problems, "Could not determine if NVM_SYMLINK is actually a symlink: "+err.Error()) } } else { if fileInfo.Mode()&os.ModeSymlink != 0 { targetPath, err := os.Readlink(symlink) if err != nil { problems = append(problems, fmt.Sprintf("SYMLINK_READ Error: %v", err)) } else { targetFileInfo, err := os.Lstat(targetPath) if err != nil { problems = append(problems, fmt.Sprintf("SYMLINK_READ Error: %v", err)) } else if !targetFileInfo.Mode().IsDir() { problems = append(problems, "NVM_SYMLINK is a symlink linking to a file instead of a directory.") } } } else { problems = append(problems, "NVM_SYMLINK ("+symlink+") is not a valid symlink.") } } 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 has been known to slow downloads significantly.") } nodelist := web.Ping(web.GetFullNodeUrl("index.json")) if !nodelist { if len(env.node_mirror) > 0 && env.node_mirror != "none" { problems = append(problems, "Connection to "+env.node_mirror+" (mirror) cannot be established. Check the mirror server to assure it is online.") } else { if len(env.proxy) > 0 { problems = append(problems, "Connection to nodejs.org cannot be established. Check your proxy ("+env.proxy+") and your physical internet connection.") } else { problems = append(problems, "Connection to nodejs.org cannot be established. Check your internet connection.") } } } invalid := make([]string, 0) invalidnpm := make([]string, 0) for i := 0; i < len(v); i++ { 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]) } } if len(invalid) > 0 { problems = append(problems, "The following Node installations are invalid (missing node.exe): "+strings.Join(invalid, ", ")+" - consider reinstalling these versions") } if len(invalidnpm) > 0 { fmt.Printf("\nWARNING: The following Node installations are missing npm: %v\n (Node will still run, but npm will not work on these versions)\n", strings.Join(invalidnpm, ", ")) } if len(env.npm_mirror) > 0 { fmt.Println("If you are experiencing npm problems, check the npm mirror (" + env.npm_mirror + ") to assure it is online and accessible.") } if _, err := os.Stat(env.settings); err != nil { problems = append(problems, "Cannot find "+env.settings) } if len(problems) == 0 { fmt.Println("\n" + "No problems detected.") } else { fmt.Println("\nPROBLEMS DETECTED\n-----------------") for _, p := range problems { fmt.Println(p + "\n") } } // 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) } for _, warning := range update.VersionWarnings { upgrade.Warn(warning, colorize) } if len(update.Warnings) > 0 || len(update.VersionWarnings) > 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") } func help() { fmt.Println("\nRunning version " + NvmVersion + ".") fmt.Println("\nUsage:") fmt.Println(" ") fmt.Println(" nvm arch : Show if node is running in 32 or 64 bit mode.") fmt.Println(" nvm current : Display active version.") fmt.Println(" nvm debug : Check the NVM4W process for known problems (troubleshooter).") fmt.Println(" nvm install [arch] : The version can be a specific version, \"latest\" for the latest current version, or \"lts\" for the") fmt.Println(" most recent LTS version. Optionally specify whether to install the 32 or 64 bit version (defaults") fmt.Println(" to system arch). Set [arch] to \"all\" to install 32 AND 64 bit versions.") fmt.Println(" Add --insecure to the end of this command to bypass SSL validation of the remote download server.") fmt.Println(" nvm list [available] : List the node.js installations. Type \"available\" at the end to see what can be installed. Aliased as ls.") fmt.Println(" nvm on : Enable node.js version management.") fmt.Println(" nvm off : Disable node.js version management.") fmt.Println(" nvm proxy [url] : Set a proxy to use for downloads. Leave [url] blank to see the current proxy.") fmt.Println(" Set [url] to \"none\" to remove the proxy.") 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 upgrade : Update nvm to the latest version. Manual rollback available for 7 days after upgrade.") 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 subscribe [--] : Subscribe to desktop notifications.") fmt.Println(" Valid topics: lts, current, nvm4w, author") fmt.Println(" nvm unsubscribe [--] : Unsubscribe from desktop notifications.") fmt.Println(" Valid topics: lts, current, nvm4w, author") fmt.Println(" nvm [--]version : Displays the current running version of nvm for Windows. Aliased as v.") fmt.Println(" ") } // =============================================================== // END | CLI functions // =============================================================== // =============================================================== // BEGIN | Utility functions // =============================================================== func checkVersionExceedsLatest(version string) bool { //content := web.GetRemoteTextFile("http://nodejs.org/dist/latest/SHASUMS256.txt") url := web.GetFullNodeUrl("latest/SHASUMS256.txt") content, err := web.GetRemoteTextFile(url) if err != nil { fmt.Println(err) os.Exit(1) } re := regexp.MustCompile("node-v(.+)+msi") reg := regexp.MustCompile("node-v|-[xa].+") latest := reg.ReplaceAllString(re.FindString(content), "") var vArr = strings.Split(version, ".") var lArr = strings.Split(latest, ".") for index := range lArr { lat, _ := strconv.Atoi(lArr[index]) ver, _ := strconv.Atoi(vArr[index]) //Should check for valid input (checking for conversion errors) but this tool is made to trust the user if ver < lat { return false } else if ver > lat { return true } } return false } func cleanVersion(version string) string { re := regexp.MustCompile("\\d+.\\d+.\\d+") matched := re.FindString(version) if len(matched) == 0 { re = regexp.MustCompile("\\d+.\\d+") matched = re.FindString(version) if len(matched) == 0 { matched = version + ".0.0" } else { matched = matched + ".0" } fmt.Println(matched) } return matched } // 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] } func getLatest() string { url := web.GetFullNodeUrl("latest/SHASUMS256.txt") content, err := web.GetRemoteTextFile(url) if err != nil { fmt.Println(err) os.Exit(1) } re := regexp.MustCompile("node-v(.+)+msi") reg := regexp.MustCompile("node-v|-[xa].+") return reg.ReplaceAllString(re.FindString(content), "") } 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() 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] } func updateRootDir(path string) { _, err := os.Stat(path) if err != nil { fmt.Println(path + " does not exist or could not be found.") return } currentRoot := env.root env.root = filepath.Clean(path) // Copy command files os.Link(filepath.Clean(currentRoot+"/elevate.cmd"), filepath.Clean(env.root+"/elevate.cmd")) os.Link(filepath.Clean(currentRoot+"/elevate.vbs"), filepath.Clean(env.root+"/elevate.vbs")) saveSettings() if currentRoot != env.root { fmt.Println("\nRoot has been changed from " + currentRoot + " to " + path) } } func elevatedRun(name string, arg ...string) (bool, error) { ok, err := run("cmd", nil, append([]string{"/C", name}, arg...)...) if err != nil { exe, _ := os.Executable() cmd := filepath.Join(filepath.Dir(exe), "elevate.cmd") ok, err = run(cmd, &env.root, append([]string{"cmd", "/C", name}, arg...)...) } return ok, err } func run(name string, dir *string, arg ...string) (bool, error) { c := exec.Command(name, arg...) if dir != nil { c.Dir = *dir } var stderr bytes.Buffer c.Stderr = &stderr err := c.Run() if err != nil { return false, errors.New(fmt.Sprint(err) + ": " + stderr.String()) } return true, nil } func runElevated(command string, forceUAC ...bool) (bool, error) { uac := true //false if len(forceUAC) > 0 { uac = forceUAC[0] } if uac { // Alternative elevation option at stackoverflow.com/questions/31558066/how-to-ask-for-administer-privileges-on-windows-with-go cmd := exec.Command(filepath.Join(env.root, "elevate.cmd"), command) var output bytes.Buffer var _stderr bytes.Buffer cmd.Stdout = &output cmd.Stderr = &_stderr perr := cmd.Run() if perr != nil { return false, errors.New(fmt.Sprint(perr) + ": " + _stderr.String()) } } c := exec.Command("cmd") // dummy executable that actually needs to exist but we'll overwrite using .SysProcAttr // Based on the official docs, syscall.SysProcAttr.CmdLine doesn't exist. // But it does and is vital: // https://github.com/golang/go/issues/15566#issuecomment-333274825 // https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773 c.SysProcAttr = &syscall.SysProcAttr{CmdLine: command} var stderr bytes.Buffer c.Stderr = &stderr err := c.Run() if err != nil { msg := stderr.String() if strings.Contains(msg, "not have sufficient privilege") && uac { return runElevated(command, false) } // fmt.Println(fmt.Sprint(err) + ": " + stderr.String()) return false, errors.New(fmt.Sprint(err) + ": " + msg) } return true, nil } func saveSettings() { content := "root: " + strings.Trim(encode(env.root), " \n\r") + "\r\narch: " + strings.Trim(encode(env.arch), " \n\r") + "\r\nproxy: " + strings.Trim(encode(env.proxy), " \n\r") + "\r\noriginalpath: " + strings.Trim(encode(env.originalpath), " \n\r") + "\r\noriginalversion: " + strings.Trim(encode(env.originalversion), " \n\r") content = content + "\r\nnode_mirror: " + strings.Trim(encode(env.node_mirror), " \n\r") + "\r\nnpm_mirror: " + strings.Trim(encode(env.npm_mirror), " \n\r") ioutil.WriteFile(env.settings, []byte(content), 0644) os.Setenv("NVM_HOME", strings.Trim(encode(env.root), " \n\r")) } func getProcessPermissions() (admin bool, elevated bool, err error) { admin = false elevated = false var sid *windows.SID err = windows.AllocateAndInitializeSid( &windows.SECURITY_NT_AUTHORITY, 2, windows.SECURITY_BUILTIN_DOMAIN_RID, windows.DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid) if err != nil { return } defer windows.FreeSid(sid) token := windows.Token(0) elevated = token.IsElevated() admin, err = token.IsMember(sid) return } func encode(val string) string { converted := encoding.ToUTF8(val) // if err != nil { // fmt.Printf("WARNING: [encoding error] - %v\n", err.Error()) // return val // } return string(converted) } // =============================================================== // END | Utility functions // =============================================================== func setup() { lines, err := file.ReadLines(env.settings) if err != nil { fmt.Println("\nERROR", err) os.Exit(1) } // Process each line and extract the value m := make(map[string]string) for _, line := range lines { line = strings.TrimSpace(line) line = os.ExpandEnv(line) res := strings.Split(line, ":") if len(res) < 2 { continue } m[res[0]] = strings.TrimSpace(strings.Join(res[1:], ":")) } if val, ok := m["root"]; ok { env.root = filepath.Clean(val) } if val, ok := m["originalpath"]; ok { env.originalpath = filepath.Clean(val) } if val, ok := m["originalversion"]; ok { env.originalversion = val } if val, ok := m["arch"]; ok { env.arch = val } if val, ok := m["node_mirror"]; ok { env.node_mirror = val } if val, ok := m["npm_mirror"]; ok { env.npm_mirror = val } if val, ok := m["proxy"]; ok { if val != "none" && val != "" { if strings.ToLower(val[0:4]) != "http" { val = "http://" + val } res, err := url.Parse(val) if err == nil { web.SetProxy(res.String(), env.verifyssl) env.proxy = res.String() } } } web.SetMirrors(env.node_mirror, env.npm_mirror) env.arch = arch.Validate(env.arch) // Make sure the directories exist _, e := os.Stat(env.root) if e != nil { fmt.Println(env.root + " could not be found or does not exist. Exiting.") return } } ================================================ FILE: src/semver/semver.go ================================================ /** * Used under the MIT License. * Semver courtesy Benedikt Lang (https://github.com/blang) */ package semver import ( "errors" "fmt" "strconv" "strings" ) const ( 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, } type Version struct { 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, "") } // Checks if v is greater than o. func (v *Version) GT(o *Version) bool { 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) } // Checks if v is less than o. func (v *Version) LT(o *Version) bool { 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) } // Compares Versions v to o: // -1 == v is less than o // 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 } } // 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 } } // 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 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) } } } return nil } // Alias for Parse, parses version string and returns a validated Version or error func New(s string) (*Version, error) { return Parse(s) } // Parses version string and returns a validated Version or error func Parse(s string) (*Version, error) { 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") } // 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 } 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 } } 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 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 } // PreRelease Version type PRVersion struct { 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) // 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 } // Compares PreRelease Versions v to o: // -1 == v is less than o // 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 } } } // PreRelease version to string func (v *PRVersion) String() string { 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 } func hasLeadingZeroes(s string) bool { 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 } ================================================ FILE: src/upgrade/check.go ================================================ 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&type=pwsh"}, {"protocol", "Open CMD Prompt", "nvm://launch?action=open_terminal&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 } ================================================ FILE: src/upgrade/notification.go ================================================ 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 } ================================================ FILE: src/upgrade/register.go ================================================ 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 } ================================================ FILE: src/upgrade/upgrade.go ================================================ package upgrade import ( "archive/zip" "crypto/md5" "encoding/json" "fmt" "io" "net/http" "nvm/author" "nvm/semver" "nvm/utility" "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" ) const ( UPDATE_URL = "https://api.github.com/repos/coreybutler/nvm-windows/releases/latest" ALERTS_URL = "https://author.io/nvm4w/feed/alerts" // 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 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"` Warnings []string `json:"notices"` VersionWarnings []string `json:"versionNotices"` SourceURL string `json:"sourceTpl"` } type Release struct { Version string `json:"name"` Assets []map[string]interface{} `json:"assets"` Publish time.Time `json:"published_at"` } 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) }() go func() { for { select { case s := <-status: if s.Warn != "" { Warn(s.Warn) } if s.Err != nil { fmt.Println(s.Err) os.Exit(1) } if s.Text != "" { fmt.Println(s.Text) } if s.Done { fmt.Println("Upgrade complete") return } } } }() time.Sleep(300 * time.Millisecond) 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.Warn != "" { display(Notification{ Message: s.Warn, Icon: "nvm", }) } 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 } 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} } 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("") } fmt.Printf("upgrading from v%s-->%s\n", version, highlight(update.Version)) status <- Status{Text: "downloading..."} } else { status <- Status{Text: "nvm is up to date", Done: true} return nil } // Make temp directory tmp, err := os.MkdirTemp("", "nvm-upgrade-*") if err != nil { status <- Status{Err: fmt.Errorf("error: failed to create temporary directory: %v\n", err)} } defer os.RemoveAll(tmp) // Download the new app source := update.SourceURL // 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: 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 status <- Status{Text: "verifying checksum..."} computedChecksum, err := computeMD5Checksum(filePath) if err != nil { status <- Status{Err: 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} } // 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)")} } status <- Status{Text: "extracting update..."} if err := unzip(filepath.Join(tmp, "assets.zip"), filepath.Join(tmp, "assets")); err != nil { status <- Status{Err: err} } // Get any additional assets if len(update.Assets) > 0 { status <- Status{Text: fmt.Sprintf("downloading %d additional assets...", len(update.Assets))} for _, asset := range update.Assets { var assetURL string if !strings.HasPrefix(asset, "http") { assetURL = update.SourceURL // assetURL = fmt.Sprintf(update.SourceURL, asset) } else { assetURL = asset } assetBody, err := get(assetURL) if err != nil { status <- Status{Err: 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 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)} } 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)} } 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} } } // 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: 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 { 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 Get() (*Update, error) { return checkForUpdate(UPDATE_URL) } 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) } // 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 { 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) 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 { status <- Status{Err: err} 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" 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) } // 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 { 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) } 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) } client := &http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { return []byte{}, err } req.Header.Set("User-Agent", "nvm-windows") req.Header.Set("Cache-Control", "no-cache") req.Header.Set("Pragma", "no-cache") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return []byte{}, fmt.Errorf("error: received status code %d", resp.StatusCode) } return io.ReadAll(resp.Body) } func checkForUpdate(url string) (*Update, error) { u := Update{Assets: []string{}, Warnings: []string{}, VersionWarnings: []string{}} r := Release{} // Make the HTTP GET request utility.DebugLogf("checking for updates at %s", url) body, err := get(url, false) if err != nil { return &u, fmt.Errorf("error: reading response body: %v", err) } // Parse JSON into the struct utility.DebugLogf("Received:\n%s", string(body)) err = json.Unmarshal(body, &r) if err != nil { return &u, fmt.Errorf("error: parsing release: %v", err) } u.Version = r.Version utility.DebugLogf("latest version: %s", u.Version) // Comment the next line when development is complete // u.Version = "2.0.0" for _, asset := range r.Assets { if value, exists := asset["name"]; exists && value.(string) == "update.exe" { u.Assets = append(u.Assets, value.(string)) } if value, exists := asset["name"]; exists && value.(string) == "nvm-noinstall.zip" { u.SourceURL = asset["browser_download_url"].(string) } } utility.DebugLogf("source URL: %s", u.SourceURL) utility.DebugLogf("assets: %v", u.Assets) // Get alerts utility.DebugLogf("downloading alerts from %s", ALERTS_URL) body, err = get(ALERTS_URL, false) if err != nil { utility.DebugLogf("alert download error: %v", err) return &u, err } utility.DebugLogf("Received:\n%s", string(body)) var alerts map[string][]interface{} err = json.Unmarshal(body, &alerts) if err != nil { utility.DebugLogf("alert parsing error: %v", err) } if value, exists := alerts["all"]; exists { for _, warning := range value { warn := warning.(map[string]interface{}) if v, exists := warn["message"]; exists { u.Warnings = append(u.Warnings, v.(string)) } } } if value, exists := alerts[u.Version]; exists { utility.DebugLogf("version warnings exist for %v\n%v", u.Version, value) for _, warning := range value { utility.DebugLogf("warning: %v", warning) warn := warning.(map[string]interface{}) if v, exists := warn["message"]; exists { u.VersionWarnings = append(u.VersionWarnings, v.(string)) } } } utility.DebugLogf("warnings: %v", u.Warnings) utility.DebugLogf("version warnings: %v", u.VersionWarnings) 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 } ================================================ FILE: src/utility/logging.go ================================================ package utility import ( "fmt" "os" "path/filepath" "runtime" "strings" "syscall" ) var debug bool = false var exe string var path string const ( // Enable virtual terminal processing on Windows (required to interpret ANSI escape codes) enableVirtualTerminalProcessing = 0x0004 BOLD = "\033[38;2;255;165;0m" TEXT = "\033[38;2;255;200;100m" RESET = "\033[0m" ) func enableANSI() { kernel32 := syscall.NewLazyDLL("kernel32.dll") setConsoleMode := kernel32.NewProc("SetConsoleMode") stdout := syscall.Stdout // Get the current console mode var mode uint32 err := syscall.GetConsoleMode(stdout, &mode) if err != nil { fmt.Println("Error getting console mode:", err) return } // Enable virtual terminal processing mode |= enableVirtualTerminalProcessing _, _, err = setConsoleMode.Call(uintptr(stdout), uintptr(mode)) if err != nil && err.Error() != "The operation completed successfully." { fmt.Println("Error enabling ANSI:", err) } } func bold(text string) string { return BOLD + text + RESET } func text(txt string) string { return TEXT + txt + RESET } func EnableDebugLogs() { debug = true exe, _ = os.Executable() path = filepath.Join(filepath.Dir(exe), "..") enableANSI() } func DebugLog(args ...interface{}) { if debug { _, file, line, _ := runtime.Caller(1) for _, arg := range args { fmt.Printf(bold("[DEBUG] %v:%v")+" "+text("%v")+"\n", strings.Replace(filepath.ToSlash(file), filepath.ToSlash(path), "..", 1), line, arg) } } } func DebugLogf(tpl string, args ...interface{}) { if debug { _, file, line, _ := runtime.Caller(1) fmt.Printf(bold("[DEBUG] %v:%v")+" "+text("%v")+"\n", strings.Replace(filepath.ToSlash(file), filepath.ToSlash(path), "..", 1), line, fmt.Sprintf(tpl, args...)) } } func DebugFn(fn func()) { if debug { fn() } } ================================================ FILE: src/utility/rename.go ================================================ package utility import ( "fmt" "io" "os" "path/filepath" ) func Rename(old, new string) error { old_drive := filepath.VolumeName(old) new_drive := filepath.VolumeName(new) if old_drive == new_drive { return os.Rename(old, new) } // Get file or directory info info, err := os.Stat(old) if err != nil { return fmt.Errorf("failed to stat source: %w", err) } // If old is a directory, copy recursively if info.IsDir() { err = copyDir(old, new) if err != nil { return fmt.Errorf("failed to copy directory: %w", err) } } else { // Otherwise, copy a single file err = copyFile(old, new) if err != nil { return fmt.Errorf("failed to copy file: %w", err) } } // Remove the original source err = os.RemoveAll(old) if err != nil { return fmt.Errorf("failed to remove source: %w", err) } return nil } // copyFile copies a single file from source (old) to destination (new). func copyFile(old, new string) error { srcFile, err := os.Open(old) if err != nil { return fmt.Errorf("failed to open source file: %w", err) } defer srcFile.Close() // Ensure destination directory exists destDir := filepath.Dir(new) err = os.MkdirAll(destDir, os.ModePerm) if err != nil { return fmt.Errorf("failed to create destination directory: %w", err) } destFile, err := os.Create(new) if err != nil { return fmt.Errorf("failed to create destination file: %w", err) } defer destFile.Close() _, err = io.Copy(destFile, srcFile) if err != nil { return fmt.Errorf("failed to copy data: %w", err) } // Copy file permissions info, err := srcFile.Stat() if err != nil { return fmt.Errorf("failed to get source file info: %w", err) } err = os.Chmod(new, info.Mode()) if err != nil { return fmt.Errorf("failed to set permissions on destination file: %w", err) } return nil } // copyDir recursively copies a directory from old path to new path. func copyDir(old, new string) error { entries, err := os.ReadDir(old) if err != nil { return fmt.Errorf("failed to read source directory: %w", err) } // Ensure destination directory exists err = os.MkdirAll(new, os.ModePerm) if err != nil { return fmt.Errorf("failed to create destination directory: %w", err) } for _, entry := range entries { srcPath := filepath.Join(old, entry.Name()) destPath := filepath.Join(new, entry.Name()) if entry.IsDir() { err = copyDir(srcPath, destPath) if err != nil { return fmt.Errorf("failed to copy subdirectory: %w", err) } } else { err = copyFile(srcPath, destPath) if err != nil { return fmt.Errorf("failed to copy file: %w", err) } } } return nil } ================================================ FILE: src/web/web.go ================================================ package web import ( "crypto/tls" "fmt" "io" "io/ioutil" "net" "net/http" "net/url" "nvm/arch" "nvm/file" "os" "os/exec" "os/signal" "path/filepath" "strconv" "strings" "syscall" "nvm/utility" "archive/zip" "github.com/blang/semver" 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/" // var oldNpmBaseAddress = "https://github.com/npm/npm/archive/" func SetProxy(p string, verifyssl bool) { if p != "" && p != "none" { proxyUrl, _ := url.Parse(p) client = &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyUrl), TLSClientConfig: &tls.Config{InsecureSkipVerify: verifyssl}}} } else { client = &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: verifyssl}}} } } func SetMirrors(node_mirror string, npm_mirror string) { if node_mirror != "" && node_mirror != "none" { nodeBaseAddress = node_mirror if strings.ToLower(nodeBaseAddress[0:4]) != "http" { nodeBaseAddress = "http://" + nodeBaseAddress } if !strings.HasSuffix(nodeBaseAddress, "/") { nodeBaseAddress = nodeBaseAddress + "/" } } if npm_mirror != "" && npm_mirror != "none" { npmBaseAddress = npm_mirror if strings.ToLower(npmBaseAddress[0:4]) != "http" { npmBaseAddress = "http://" + npmBaseAddress } if !strings.HasSuffix(npmBaseAddress, "/") { npmBaseAddress = npmBaseAddress + "/" } } } func GetFullNodeUrl(path string) string { return nodeBaseAddress + path } func GetFullNpmUrl(path string) string { return npmBaseAddress + path } func IsLocalIPv6() (bool, error) { conn, err := net.Dial("tcp", "[::1]:80") if err != nil { if strings.Contains(strings.ToLower(err.Error()), "no connection") { return false, nil } return false, err } defer conn.Close() return true, nil // addrs, err := net.InterfaceAddrs() // if err != nil { // return false, err // } // for _, addr := range addrs { // fmt.Println(addr.String()) // if strings.Contains(addr.String(), ":") { // return true, nil // } // } // return false, nil } // Returns whether the address can be pinged and whether it is using IPv6 or not func Ping(url string) bool { req, err := http.NewRequest("HEAD", url, nil) if err != nil { fmt.Println(err) return false } req.Header.Set("User-Agent", "NVM for Windows") response, err := client.Do(req) if err != nil { return false } if response.StatusCode == 200 { return true } return false } func Download(url string, target string, version string) bool { output, err := os.Create(target) if err != nil { fmt.Println("Error while creating", target, "-", err) return false } defer output.Close() req, err := http.NewRequest("GET", url, nil) if err != nil { fmt.Println(err) return false } req.Header.Set("User-Agent", fmt.Sprintf("NVM for Windows %s", nvmversion)) response, err := client.Do(req) if err != nil { fmt.Println("Error while downloading", url, "-", err) return false } defer response.Body.Close() c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c fmt.Println("Download interrupted. Rolling back...") output.Close() response.Body.Close() var err error if strings.Contains(target, "node") { err = os.RemoveAll(os.Getenv("NVM_HOME") + "\\v" + version) } else { err = os.Remove(target) } if err != nil { fmt.Println("Error while rolling back", err) } os.Exit(1) }() var body []byte if response.StatusCode != 200 { body, err = ioutil.ReadAll(response.Body) if err != nil { fmt.Println("Failed to read response body: " + err.Error()) } } else { _, err = io.Copy(output, response.Body) if err != nil { fmt.Printf("Error while downloading %s: %v\n", url, err) } } redirect := response.Header.Get("Location") switch response.StatusCode { case 300: if len(redirect) > 0 && redirect != url { return Download(redirect, target, version) } if strings.Contains(url, "/npm/cli/archive/v6.14.17.zip") { return Download("https://github.com/npm/cli/archive/refs/tags/v6.14.17.zip", target, version) } fmt.Printf("\n\nREMOTE SERVER FAILURE\n\n---\nGET %v --> %v\n\n", url, response.StatusCode) for key, val := range response.Header { fmt.Printf("%v: %v\n", key, val) } if len(body) > 0 { fmt.Printf("\n%s", body) } fmt.Println("\n---\n\n") return false case 302: fallthrough case 307: fmt.Println("Redirecting to " + redirect) return Download(redirect, target, version) case 200: // No processing necessary for successful response default: fmt.Println("Download failed. Rolling Back.") err := os.Remove(target) if err != nil { fmt.Println(target) fmt.Println("Rollback failed.", err) } return false } return true } func GetNodeJS(root string, v string, a string, append bool) bool { utility.DebugLogf("running GetNodeJS with root: %v, v%v, arch: %v, append: %v", root, v, a, append) a = arch.Validate(a) vpre := "" vers := strings.Fields(strings.Replace(v, ".", " ", -1)) main, _ := strconv.ParseInt(vers[0], 0, 0) if a == "32" { if main > 0 { vpre = "win-x86/" } else { vpre = "" } } else if a == "64" { if main > 0 { vpre = "win-x64/" } else { vpre = "x64/" } } else if a == "arm64" { if main > 0 { vpre = "win-arm64/" } else { vpre = "arm64/" } } url := getNodeUrl(v, vpre, a, append) utility.DebugLogf("download url: %v", url) if url == "" { //No url should mean this version/arch isn't available fmt.Println("Node.js v" + v + " " + a + "bit isn't available right now.") } else { fileName := root + "\\v" + v + "\\node" + a + ".exe" if strings.HasSuffix(url, ".zip") { fileName = root + "\\v" + v + "\\node.zip" } fmt.Println("Downloading node.js version " + v + " (" + a + "-bit)... ") if Download(url, fileName, v) { utility.DebugLog("download succeeded") // Extract the zip file if strings.HasSuffix(url, ".zip") { fmt.Println("Extracting node and npm...") utility.DebugLogf("extracting %v to %v", fileName, root+"\\v"+v) err := unzip(fileName, root+"\\v"+v) if err != nil { fmt.Println("Error extracting from Node archive: " + err.Error()) err = os.Remove(fileName) if err != nil { fmt.Printf("Failed to remove %v after failed extraction. Please remove manually.", fileName) } utility.DebugLogf("removed %v", fileName) return false } err = os.Remove(fileName) if err != nil { fmt.Printf("Failed to remove %v after successful extraction. Please remove manually.", fileName) } utility.DebugLogf("removed %v", fileName) zip := root + "\\v" + v + "\\" + strings.Replace(filepath.Base(url), ".zip", "", 1) utility.DebugLogf("moving %v to %v", zip, root+"\\v"+v) err = fs.Move(zip, root+"\\v"+v, true) if err != nil { fmt.Println("ERROR moving file: " + err.Error()) } utility.DebugLog("move succeeded") err = os.RemoveAll(zip) if err != nil { fmt.Printf("Failed to remove %v after successful extraction. Please remove manually.", zip) } utility.DebugLogf("removed %v", zip) utility.DebugFn(func() { cmd := exec.Command("cmd", "/C", "dir", root+"\\v"+v) out, err := cmd.CombinedOutput() if err != nil { utility.DebugLog(err.Error()) } else { utility.DebugLog(string(out)) } }) } fmt.Println("Complete") return true } else { utility.DebugLog("download failed") return false } } return false } func GetNpm(root string, v string) bool { url := GetFullNpmUrl("v" + v + ".zip") // temp directory to download the .zip file tempDir := root + "\\temp" utility.DebugLogf("downloading npm from %v to %v", url, tempDir) // if the temp directory doesn't exist, create it if !file.Exists(tempDir) { fmt.Println("Creating " + tempDir + "\n") err := os.Mkdir(tempDir, os.ModePerm) if err != nil { fmt.Println(err) os.Exit(1) } } fileName := tempDir + "\\" + "npm-v" + v + ".zip" fmt.Printf("Downloading npm version " + v + "... ") if Download(url, fileName, v) { utility.DebugLog("npm download succeeded") fmt.Printf("Complete\n") return true } else { utility.DebugLog("npm download failed") return false } } func GetRemoteTextFile(url string) (string, error) { response, httperr := client.Get(url) if httperr != nil { return "", fmt.Errorf("Could not retrieve %v: %v", url, httperr) } if response.StatusCode != 200 { return "", fmt.Errorf("Error retrieving \"%s\": HTTP Status %v\n", url, response.StatusCode) } defer response.Body.Close() contents, readerr := ioutil.ReadAll(response.Body) if readerr != nil { return "", fmt.Errorf("error reading HTTP request body: %v", readerr) } return string(contents), nil } func IsNode64bitAvailable(v string) bool { if v == "latest" { return true } // Anything below version 8 doesn't have a 64 bit version vers := strings.Fields(strings.Replace(v, ".", " ", -1)) main, _ := strconv.ParseInt(vers[0], 0, 0) minor, _ := strconv.ParseInt(vers[1], 0, 0) if main == 0 && minor < 8 { return false } // TODO: fixme. Assume a 64 bit version exists return true } func IsNodeArm64bitAvailable(v string) bool { if v == "latest" { return true } // Anything below version 19.9 doesn't have a arm64 bit version vers := strings.Fields(strings.Replace(v, ".", " ", -1)) main, _ := strconv.ParseInt(vers[0], 0, 0) minor, _ := strconv.ParseInt(vers[1], 0, 0) fmt.Println("main " + strconv.FormatInt(main, 10) + " minor " + strconv.FormatInt(minor, 10)) if main < 19 { return false } if main == 19 && minor < 9 { return false } // TODO: fixme. Assume a 64 bit version exists return true } func getNodeUrl(v string, vpre string, arch string, append bool) string { a := "x86" if arch == "arm64" { a = "arm64" } if arch == "64" { a = "x64" } //url := "http://nodejs.org/dist/v"+v+"/" + vpre + "/node.exe" url := GetFullNodeUrl("v" + v + "/" + vpre + "node.exe") if !append { version, err := semver.Make(v) if err != nil { fmt.Println("Node.js v" + v + " " + a + "bit isn't available right now.") fmt.Println(err.Error()) os.Exit(1) } corepack, _ := semver.Make("16.9.0") if version.GTE(corepack) { url = GetFullNodeUrl("v" + v + "/node-v" + v + "-win-" + a + ".zip") } } // Check online to see if a 64 bit version exists _, err := client.Head(url) if err != nil { return "" } return url } func unzip(src, dest string) error { r, err := zip.OpenReader(src) if err != nil { return err } defer func() { if err := r.Close(); err != nil { panic(err) } }() os.MkdirAll(dest, 0755) // Closure to address file descriptors issue with all the deferred .Close() methods extractAndWriteFile := func(f *zip.File) error { rc, err := f.Open() if err != nil { return err } defer func() { if err := rc.Close(); err != nil { panic(err) } }() path := filepath.Join(dest, f.Name) // Check for ZipSlip (Directory traversal) if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) { return fmt.Errorf("illegal file path: %s", path) } if f.FileInfo().IsDir() { os.MkdirAll(path, f.Mode()) } else { os.MkdirAll(filepath.Dir(path), f.Mode()) f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return err } defer func() { if err := f.Close(); err != nil { panic(err) } }() _, err = io.Copy(f, rc) if err != nil { return err } } return nil } for _, f := range r.File { err := extractAndWriteFile(f) if err != nil { return err } } return nil }