├── .editorconfig ├── .github ├── renovate.json5 └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── Taskfile.yml ├── rustfmt.toml ├── src ├── archives.rs ├── constants.rs ├── files │ ├── mod.rs │ └── package_json.rs ├── main.rs ├── node_version.rs └── subcommand │ ├── install.rs │ ├── is_installed.rs │ ├── list.rs │ ├── mod.rs │ ├── parse_version.rs │ ├── switch.rs │ └── uninstall.rs ├── test-data └── node-versions.json └── tests ├── install_test.rs ├── switch_test.rs ├── uninstall_test.rs └── utils.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | trim_trailing_whitespace=true 5 | insert_final_newline=true 6 | indent_style=space 7 | indent_size=2 8 | tab_width=2 9 | 10 | [*.rs] 11 | indent_size=4 12 | tab_width=4 13 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: ["config:base", ":scheduleMonthly"], 4 | prHourlyLimit: 5, 5 | prConcurrentLimit: 5, 6 | branchConcurrentLimit: 5, 7 | labels: ["dependencies"], 8 | baseBranches: ["main"], 9 | packageRules: [ 10 | { 11 | matchUpdateTypes: ["patch", "minor"], 12 | matchManagers: ["cargo"], 13 | automerge: true, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [ push ] 2 | 3 | name: ci 4 | 5 | env: 6 | FORCE_COLOR: 3 7 | TERM: xterm-256color 8 | 9 | jobs: 10 | build: 11 | name: build (${{ matrix.os }}) 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - os: macos-latest 18 | file-name: nvm 19 | - os: ubuntu-latest 20 | file-name: nvm 21 | - os: windows-latest 22 | file-name: nvm.exe 23 | 24 | runs-on: ${{ matrix.os }} 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - uses: arduino/setup-task@v1 30 | with: 31 | repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 32 | 33 | - uses: dtolnay/rust-toolchain@master 34 | with: 35 | toolchain: nightly 36 | 37 | - uses: Swatinem/rust-cache@v2 38 | 39 | - run: task build:release 40 | 41 | - name: Upload artifacts 42 | uses: actions/upload-artifact@v3 43 | with: 44 | name: nvm-${{ matrix.os }} 45 | path: target/release/${{ matrix.file-name }} 46 | 47 | test: 48 | timeout-minutes: 15 49 | continue-on-error: true 50 | strategy: 51 | matrix: 52 | os: [ macos-latest, ubuntu-latest, windows-latest ] 53 | 54 | runs-on: ${{ matrix.os }} 55 | 56 | steps: 57 | - uses: actions/checkout@v4 58 | 59 | - uses: arduino/setup-task@v1 60 | with: 61 | repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 62 | 63 | - uses: dtolnay/rust-toolchain@master 64 | with: 65 | toolchain: nightly 66 | 67 | - uses: Swatinem/rust-cache@v2 68 | 69 | - run: task test 70 | 71 | clippy: 72 | runs-on: ubuntu-latest 73 | 74 | steps: 75 | - uses: actions/checkout@v4 76 | 77 | - uses: arduino/setup-task@v1 78 | with: 79 | repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 80 | 81 | - uses: dtolnay/rust-toolchain@master 82 | with: 83 | toolchain: nightly 84 | components: rustfmt, clippy 85 | 86 | - run: task format -- --check 87 | 88 | - uses: actions-rs/clippy-check@v1 89 | with: 90 | token: ${{ secrets.GITHUB_TOKEN }} 91 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | tags: 5 | - v* 6 | 7 | name: release 8 | 9 | env: 10 | FORCE_COLOR: 3 11 | TERM: xterm-256color 12 | 13 | jobs: 14 | create-release: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: dtolnay/rust-toolchain@master 21 | with: 22 | toolchain: nightly 23 | 24 | - uses: Swatinem/rust-cache@v2 25 | 26 | - run: cargo publish 27 | env: 28 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | 30 | - uses: softprops/action-gh-release@v1 31 | with: 32 | draft: true 33 | 34 | 35 | build: 36 | name: build (${{ matrix.os }}) 37 | 38 | needs: [create-release] 39 | 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | include: 44 | - os: macos-latest 45 | file-name: nvm 46 | display-name: nvm-macos 47 | - os: ubuntu-latest 48 | file-name: nvm 49 | display-name: nvm-linux 50 | - os: windows-latest 51 | file-name: nvm.exe 52 | display-name: nvm-win.exe 53 | 54 | runs-on: ${{ matrix.os }} 55 | 56 | steps: 57 | - uses: actions/checkout@v4 58 | 59 | - uses: arduino/setup-task@v1 60 | with: 61 | repo-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 62 | 63 | - uses: dtolnay/rust-toolchain@master 64 | with: 65 | toolchain: nightly 66 | 67 | - uses: Swatinem/rust-cache@v2 68 | 69 | - run: task build:release 70 | 71 | - run: mv target/release/${{ matrix.file-name }} target/release/${{ matrix.display-name }} 72 | 73 | - uses: softprops/action-gh-release@v1 74 | with: 75 | draft: true 76 | files: target/release/${{ matrix.display-name }} 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .task 3 | /target 4 | integration 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aes" 13 | version = "0.8.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" 16 | dependencies = [ 17 | "cfg-if", 18 | "cipher", 19 | "cpufeatures", 20 | ] 21 | 22 | [[package]] 23 | name = "aho-corasick" 24 | version = "0.7.18" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 27 | dependencies = [ 28 | "memchr", 29 | ] 30 | 31 | [[package]] 32 | name = "anstream" 33 | version = "0.6.1" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "f6cd65a4b849ace0b7f6daeebcc1a1d111282227ca745458c61dbf670e52a597" 36 | dependencies = [ 37 | "anstyle", 38 | "anstyle-parse", 39 | "anstyle-query", 40 | "anstyle-wincon", 41 | "colorchoice", 42 | "utf8parse", 43 | ] 44 | 45 | [[package]] 46 | name = "anstyle" 47 | version = "1.0.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 50 | 51 | [[package]] 52 | name = "anstyle-parse" 53 | version = "0.2.0" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 56 | dependencies = [ 57 | "utf8parse", 58 | ] 59 | 60 | [[package]] 61 | name = "anstyle-query" 62 | version = "1.0.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 65 | dependencies = [ 66 | "windows-sys 0.48.0", 67 | ] 68 | 69 | [[package]] 70 | name = "anstyle-wincon" 71 | version = "3.0.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "0238ca56c96dfa37bdf7c373c8886dd591322500aceeeccdb2216fe06dc2f796" 74 | dependencies = [ 75 | "anstyle", 76 | "windows-sys 0.48.0", 77 | ] 78 | 79 | [[package]] 80 | name = "anyhow" 81 | version = "1.0.75" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 84 | 85 | [[package]] 86 | name = "assert_cmd" 87 | version = "2.0.12" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" 90 | dependencies = [ 91 | "anstyle", 92 | "bstr 1.0.1", 93 | "doc-comment", 94 | "predicates", 95 | "predicates-core", 96 | "predicates-tree", 97 | "wait-timeout", 98 | ] 99 | 100 | [[package]] 101 | name = "assert_fs" 102 | version = "1.0.13" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" 105 | dependencies = [ 106 | "anstyle", 107 | "doc-comment", 108 | "globwalk", 109 | "predicates", 110 | "predicates-core", 111 | "predicates-tree", 112 | "tempfile", 113 | ] 114 | 115 | [[package]] 116 | name = "autocfg" 117 | version = "1.0.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 120 | 121 | [[package]] 122 | name = "base64" 123 | version = "0.21.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 126 | 127 | [[package]] 128 | name = "base64ct" 129 | version = "1.0.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" 132 | 133 | [[package]] 134 | name = "bitflags" 135 | version = "1.3.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 138 | 139 | [[package]] 140 | name = "block-buffer" 141 | version = "0.10.2" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" 144 | dependencies = [ 145 | "generic-array", 146 | ] 147 | 148 | [[package]] 149 | name = "bstr" 150 | version = "0.2.17" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 153 | dependencies = [ 154 | "memchr", 155 | ] 156 | 157 | [[package]] 158 | name = "bstr" 159 | version = "1.0.1" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd" 162 | dependencies = [ 163 | "memchr", 164 | "once_cell", 165 | "regex-automata", 166 | "serde", 167 | ] 168 | 169 | [[package]] 170 | name = "bumpalo" 171 | version = "3.12.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 174 | 175 | [[package]] 176 | name = "bytecount" 177 | version = "0.6.2" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" 180 | 181 | [[package]] 182 | name = "byteorder" 183 | version = "1.4.3" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 186 | 187 | [[package]] 188 | name = "bzip2" 189 | version = "0.4.3" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" 192 | dependencies = [ 193 | "bzip2-sys", 194 | "libc", 195 | ] 196 | 197 | [[package]] 198 | name = "bzip2-sys" 199 | version = "0.1.11+1.0.8" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" 202 | dependencies = [ 203 | "cc", 204 | "libc", 205 | "pkg-config", 206 | ] 207 | 208 | [[package]] 209 | name = "cc" 210 | version = "1.0.71" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" 213 | dependencies = [ 214 | "jobserver", 215 | ] 216 | 217 | [[package]] 218 | name = "cfg-if" 219 | version = "1.0.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 222 | 223 | [[package]] 224 | name = "cipher" 225 | version = "0.4.4" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 228 | dependencies = [ 229 | "crypto-common", 230 | "inout", 231 | ] 232 | 233 | [[package]] 234 | name = "clap" 235 | version = "4.4.11" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" 238 | dependencies = [ 239 | "clap_builder", 240 | "clap_derive", 241 | ] 242 | 243 | [[package]] 244 | name = "clap_builder" 245 | version = "4.4.11" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" 248 | dependencies = [ 249 | "anstream", 250 | "anstyle", 251 | "clap_lex", 252 | "strsim", 253 | ] 254 | 255 | [[package]] 256 | name = "clap_derive" 257 | version = "4.4.7" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 260 | dependencies = [ 261 | "heck", 262 | "proc-macro2", 263 | "quote", 264 | "syn 2.0.28", 265 | ] 266 | 267 | [[package]] 268 | name = "clap_lex" 269 | version = "0.6.0" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 272 | 273 | [[package]] 274 | name = "colorchoice" 275 | version = "1.0.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 278 | 279 | [[package]] 280 | name = "console" 281 | version = "0.15.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" 284 | dependencies = [ 285 | "encode_unicode", 286 | "libc", 287 | "once_cell", 288 | "regex", 289 | "terminal_size", 290 | "unicode-width", 291 | "winapi", 292 | ] 293 | 294 | [[package]] 295 | name = "constant_time_eq" 296 | version = "0.1.5" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 299 | 300 | [[package]] 301 | name = "core-foundation" 302 | version = "0.9.3" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 305 | dependencies = [ 306 | "core-foundation-sys", 307 | "libc", 308 | ] 309 | 310 | [[package]] 311 | name = "core-foundation-sys" 312 | version = "0.8.3" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 315 | 316 | [[package]] 317 | name = "cpufeatures" 318 | version = "0.2.2" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" 321 | dependencies = [ 322 | "libc", 323 | ] 324 | 325 | [[package]] 326 | name = "crc32fast" 327 | version = "1.3.2" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 330 | dependencies = [ 331 | "cfg-if", 332 | ] 333 | 334 | [[package]] 335 | name = "crossbeam-utils" 336 | version = "0.8.8" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" 339 | dependencies = [ 340 | "cfg-if", 341 | "lazy_static", 342 | ] 343 | 344 | [[package]] 345 | name = "crypto-common" 346 | version = "0.1.6" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 349 | dependencies = [ 350 | "generic-array", 351 | "typenum", 352 | ] 353 | 354 | [[package]] 355 | name = "dialoguer" 356 | version = "0.11.0" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" 359 | dependencies = [ 360 | "console", 361 | "shell-words", 362 | "tempfile", 363 | "thiserror", 364 | "zeroize", 365 | ] 366 | 367 | [[package]] 368 | name = "difflib" 369 | version = "0.4.0" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 372 | 373 | [[package]] 374 | name = "digest" 375 | version = "0.10.3" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" 378 | dependencies = [ 379 | "block-buffer", 380 | "crypto-common", 381 | "subtle", 382 | ] 383 | 384 | [[package]] 385 | name = "dirs" 386 | version = "4.0.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 389 | dependencies = [ 390 | "dirs-sys", 391 | ] 392 | 393 | [[package]] 394 | name = "dirs-sys" 395 | version = "0.3.6" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" 398 | dependencies = [ 399 | "libc", 400 | "redox_users", 401 | "winapi", 402 | ] 403 | 404 | [[package]] 405 | name = "doc-comment" 406 | version = "0.3.3" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 409 | 410 | [[package]] 411 | name = "either" 412 | version = "1.6.1" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 415 | 416 | [[package]] 417 | name = "encode_unicode" 418 | version = "0.3.6" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 421 | 422 | [[package]] 423 | name = "filetime" 424 | version = "0.2.15" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" 427 | dependencies = [ 428 | "cfg-if", 429 | "libc", 430 | "redox_syscall", 431 | "winapi", 432 | ] 433 | 434 | [[package]] 435 | name = "flate2" 436 | version = "1.0.28" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" 439 | dependencies = [ 440 | "crc32fast", 441 | "miniz_oxide", 442 | ] 443 | 444 | [[package]] 445 | name = "float-cmp" 446 | version = "0.9.0" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 449 | dependencies = [ 450 | "num-traits", 451 | ] 452 | 453 | [[package]] 454 | name = "fnv" 455 | version = "1.0.7" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 458 | 459 | [[package]] 460 | name = "form_urlencoded" 461 | version = "1.0.1" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 464 | dependencies = [ 465 | "matches", 466 | "percent-encoding", 467 | ] 468 | 469 | [[package]] 470 | name = "fuchsia-cprng" 471 | version = "0.1.1" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 474 | 475 | [[package]] 476 | name = "generic-array" 477 | version = "0.14.5" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" 480 | dependencies = [ 481 | "typenum", 482 | "version_check", 483 | ] 484 | 485 | [[package]] 486 | name = "getrandom" 487 | version = "0.2.10" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 490 | dependencies = [ 491 | "cfg-if", 492 | "libc", 493 | "wasi", 494 | ] 495 | 496 | [[package]] 497 | name = "globset" 498 | version = "0.4.8" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" 501 | dependencies = [ 502 | "aho-corasick", 503 | "bstr 0.2.17", 504 | "fnv", 505 | "log", 506 | "regex", 507 | ] 508 | 509 | [[package]] 510 | name = "globwalk" 511 | version = "0.8.1" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" 514 | dependencies = [ 515 | "bitflags", 516 | "ignore", 517 | "walkdir", 518 | ] 519 | 520 | [[package]] 521 | name = "heck" 522 | version = "0.4.0" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 525 | 526 | [[package]] 527 | name = "hmac" 528 | version = "0.12.1" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 531 | dependencies = [ 532 | "digest", 533 | ] 534 | 535 | [[package]] 536 | name = "idna" 537 | version = "0.2.3" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 540 | dependencies = [ 541 | "matches", 542 | "unicode-bidi", 543 | "unicode-normalization", 544 | ] 545 | 546 | [[package]] 547 | name = "ignore" 548 | version = "0.4.18" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" 551 | dependencies = [ 552 | "crossbeam-utils", 553 | "globset", 554 | "lazy_static", 555 | "log", 556 | "memchr", 557 | "regex", 558 | "same-file", 559 | "thread_local", 560 | "walkdir", 561 | "winapi-util", 562 | ] 563 | 564 | [[package]] 565 | name = "inout" 566 | version = "0.1.3" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 569 | dependencies = [ 570 | "generic-array", 571 | ] 572 | 573 | [[package]] 574 | name = "itertools" 575 | version = "0.11.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" 578 | dependencies = [ 579 | "either", 580 | ] 581 | 582 | [[package]] 583 | name = "itertools" 584 | version = "0.12.0" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" 587 | dependencies = [ 588 | "either", 589 | ] 590 | 591 | [[package]] 592 | name = "itoa" 593 | version = "1.0.1" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 596 | 597 | [[package]] 598 | name = "jobserver" 599 | version = "0.1.24" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" 602 | dependencies = [ 603 | "libc", 604 | ] 605 | 606 | [[package]] 607 | name = "js-sys" 608 | version = "0.3.55" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" 611 | dependencies = [ 612 | "wasm-bindgen", 613 | ] 614 | 615 | [[package]] 616 | name = "lazy_static" 617 | version = "1.4.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 620 | 621 | [[package]] 622 | name = "libc" 623 | version = "0.2.147" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 626 | 627 | [[package]] 628 | name = "log" 629 | version = "0.4.14" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 632 | dependencies = [ 633 | "cfg-if", 634 | ] 635 | 636 | [[package]] 637 | name = "matches" 638 | version = "0.1.9" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 641 | 642 | [[package]] 643 | name = "memchr" 644 | version = "2.4.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 647 | 648 | [[package]] 649 | name = "miette" 650 | version = "5.3.0" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "a28d6092d7e94a90bb9ea8e6c26c99d5d112d49dda2afdb4f7ea8cf09e1a5a6d" 653 | dependencies = [ 654 | "miette-derive", 655 | "once_cell", 656 | "thiserror", 657 | "unicode-width", 658 | ] 659 | 660 | [[package]] 661 | name = "miette-derive" 662 | version = "5.3.0" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "4f2485ed7d1fe80704928e3eb86387439609bd0c6bb96db8208daa364cfd1e09" 665 | dependencies = [ 666 | "proc-macro2", 667 | "quote", 668 | "syn 1.0.105", 669 | ] 670 | 671 | [[package]] 672 | name = "minimal-lexical" 673 | version = "0.2.1" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 676 | 677 | [[package]] 678 | name = "miniz_oxide" 679 | version = "0.7.1" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 682 | dependencies = [ 683 | "adler", 684 | ] 685 | 686 | [[package]] 687 | name = "node-semver" 688 | version = "2.1.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "84f390c1756333538f2aed01cf280a56bc683e199b9804a504df6e7320d40116" 691 | dependencies = [ 692 | "bytecount", 693 | "miette", 694 | "nom", 695 | "serde", 696 | "thiserror", 697 | ] 698 | 699 | [[package]] 700 | name = "nom" 701 | version = "7.1.1" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 704 | dependencies = [ 705 | "memchr", 706 | "minimal-lexical", 707 | ] 708 | 709 | [[package]] 710 | name = "normalize-line-endings" 711 | version = "0.3.0" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 714 | 715 | [[package]] 716 | name = "num" 717 | version = "0.1.42" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" 720 | dependencies = [ 721 | "num-bigint", 722 | "num-complex", 723 | "num-integer", 724 | "num-iter", 725 | "num-rational", 726 | "num-traits", 727 | ] 728 | 729 | [[package]] 730 | name = "num-bigint" 731 | version = "0.1.44" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" 734 | dependencies = [ 735 | "num-integer", 736 | "num-traits", 737 | "rand 0.4.6", 738 | "rustc-serialize", 739 | ] 740 | 741 | [[package]] 742 | name = "num-complex" 743 | version = "0.1.43" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" 746 | dependencies = [ 747 | "num-traits", 748 | "rustc-serialize", 749 | ] 750 | 751 | [[package]] 752 | name = "num-integer" 753 | version = "0.1.45" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 756 | dependencies = [ 757 | "autocfg", 758 | "num-traits", 759 | ] 760 | 761 | [[package]] 762 | name = "num-iter" 763 | version = "0.1.43" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" 766 | dependencies = [ 767 | "autocfg", 768 | "num-integer", 769 | "num-traits", 770 | ] 771 | 772 | [[package]] 773 | name = "num-rational" 774 | version = "0.1.42" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" 777 | dependencies = [ 778 | "num-bigint", 779 | "num-integer", 780 | "num-traits", 781 | "rustc-serialize", 782 | ] 783 | 784 | [[package]] 785 | name = "num-traits" 786 | version = "0.2.14" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 789 | dependencies = [ 790 | "autocfg", 791 | ] 792 | 793 | [[package]] 794 | name = "num_threads" 795 | version = "0.1.6" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 798 | dependencies = [ 799 | "libc", 800 | ] 801 | 802 | [[package]] 803 | name = "nvm-rust" 804 | version = "0.4.3" 805 | dependencies = [ 806 | "anyhow", 807 | "assert_cmd", 808 | "assert_fs", 809 | "clap", 810 | "dialoguer", 811 | "dirs", 812 | "flate2", 813 | "itertools 0.12.0", 814 | "node-semver", 815 | "predicates", 816 | "serde", 817 | "serde_json", 818 | "spectral", 819 | "tar", 820 | "ureq", 821 | "zip", 822 | ] 823 | 824 | [[package]] 825 | name = "once_cell" 826 | version = "1.16.0" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 829 | 830 | [[package]] 831 | name = "openssl-probe" 832 | version = "0.1.5" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 835 | 836 | [[package]] 837 | name = "password-hash" 838 | version = "0.4.2" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" 841 | dependencies = [ 842 | "base64ct", 843 | "rand_core 0.6.3", 844 | "subtle", 845 | ] 846 | 847 | [[package]] 848 | name = "pbkdf2" 849 | version = "0.11.0" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" 852 | dependencies = [ 853 | "digest", 854 | "hmac", 855 | "password-hash", 856 | "sha2", 857 | ] 858 | 859 | [[package]] 860 | name = "percent-encoding" 861 | version = "2.1.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 864 | 865 | [[package]] 866 | name = "pkg-config" 867 | version = "0.3.20" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" 870 | 871 | [[package]] 872 | name = "ppv-lite86" 873 | version = "0.2.10" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 876 | 877 | [[package]] 878 | name = "predicates" 879 | version = "3.0.4" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" 882 | dependencies = [ 883 | "anstyle", 884 | "difflib", 885 | "float-cmp", 886 | "itertools 0.11.0", 887 | "normalize-line-endings", 888 | "predicates-core", 889 | "regex", 890 | ] 891 | 892 | [[package]] 893 | name = "predicates-core" 894 | version = "1.0.6" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 897 | 898 | [[package]] 899 | name = "predicates-tree" 900 | version = "1.0.4" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7" 903 | dependencies = [ 904 | "predicates-core", 905 | "termtree", 906 | ] 907 | 908 | [[package]] 909 | name = "proc-macro2" 910 | version = "1.0.63" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" 913 | dependencies = [ 914 | "unicode-ident", 915 | ] 916 | 917 | [[package]] 918 | name = "quote" 919 | version = "1.0.29" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" 922 | dependencies = [ 923 | "proc-macro2", 924 | ] 925 | 926 | [[package]] 927 | name = "rand" 928 | version = "0.4.6" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 931 | dependencies = [ 932 | "fuchsia-cprng", 933 | "libc", 934 | "rand_core 0.3.1", 935 | "rdrand", 936 | "winapi", 937 | ] 938 | 939 | [[package]] 940 | name = "rand" 941 | version = "0.8.4" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 944 | dependencies = [ 945 | "libc", 946 | "rand_chacha", 947 | "rand_core 0.6.3", 948 | "rand_hc", 949 | ] 950 | 951 | [[package]] 952 | name = "rand_chacha" 953 | version = "0.3.1" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 956 | dependencies = [ 957 | "ppv-lite86", 958 | "rand_core 0.6.3", 959 | ] 960 | 961 | [[package]] 962 | name = "rand_core" 963 | version = "0.3.1" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 966 | dependencies = [ 967 | "rand_core 0.4.2", 968 | ] 969 | 970 | [[package]] 971 | name = "rand_core" 972 | version = "0.4.2" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 975 | 976 | [[package]] 977 | name = "rand_core" 978 | version = "0.6.3" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 981 | dependencies = [ 982 | "getrandom", 983 | ] 984 | 985 | [[package]] 986 | name = "rand_hc" 987 | version = "0.3.1" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 990 | dependencies = [ 991 | "rand_core 0.6.3", 992 | ] 993 | 994 | [[package]] 995 | name = "rdrand" 996 | version = "0.4.0" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 999 | dependencies = [ 1000 | "rand_core 0.3.1", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "redox_syscall" 1005 | version = "0.2.10" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 1008 | dependencies = [ 1009 | "bitflags", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "redox_users" 1014 | version = "0.4.0" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" 1017 | dependencies = [ 1018 | "getrandom", 1019 | "redox_syscall", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "regex" 1024 | version = "1.5.6" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 1027 | dependencies = [ 1028 | "aho-corasick", 1029 | "memchr", 1030 | "regex-syntax", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "regex-automata" 1035 | version = "0.1.10" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1038 | 1039 | [[package]] 1040 | name = "regex-syntax" 1041 | version = "0.6.26" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 1044 | 1045 | [[package]] 1046 | name = "remove_dir_all" 1047 | version = "0.5.3" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1050 | dependencies = [ 1051 | "winapi", 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "ring" 1056 | version = "0.16.20" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 1059 | dependencies = [ 1060 | "cc", 1061 | "libc", 1062 | "once_cell", 1063 | "spin 0.5.2", 1064 | "untrusted 0.7.1", 1065 | "web-sys", 1066 | "winapi", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "ring" 1071 | version = "0.17.3" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" 1074 | dependencies = [ 1075 | "cc", 1076 | "getrandom", 1077 | "libc", 1078 | "spin 0.9.8", 1079 | "untrusted 0.9.0", 1080 | "windows-sys 0.48.0", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "rustc-serialize" 1085 | version = "0.3.24" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" 1088 | 1089 | [[package]] 1090 | name = "rustls" 1091 | version = "0.21.9" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" 1094 | dependencies = [ 1095 | "log", 1096 | "ring 0.17.3", 1097 | "rustls-webpki", 1098 | "sct", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "rustls-native-certs" 1103 | version = "0.6.2" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" 1106 | dependencies = [ 1107 | "openssl-probe", 1108 | "rustls-pemfile", 1109 | "schannel", 1110 | "security-framework", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "rustls-pemfile" 1115 | version = "1.0.2" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" 1118 | dependencies = [ 1119 | "base64", 1120 | ] 1121 | 1122 | [[package]] 1123 | name = "rustls-webpki" 1124 | version = "0.101.7" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 1127 | dependencies = [ 1128 | "ring 0.17.3", 1129 | "untrusted 0.9.0", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "ryu" 1134 | version = "1.0.5" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 1137 | 1138 | [[package]] 1139 | name = "same-file" 1140 | version = "1.0.6" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1143 | dependencies = [ 1144 | "winapi-util", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "schannel" 1149 | version = "0.1.21" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" 1152 | dependencies = [ 1153 | "windows-sys 0.42.0", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "sct" 1158 | version = "0.7.0" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 1161 | dependencies = [ 1162 | "ring 0.16.20", 1163 | "untrusted 0.7.1", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "security-framework" 1168 | version = "2.8.1" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "7c4437699b6d34972de58652c68b98cb5b53a4199ab126db8e20ec8ded29a721" 1171 | dependencies = [ 1172 | "bitflags", 1173 | "core-foundation", 1174 | "core-foundation-sys", 1175 | "libc", 1176 | "security-framework-sys", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "security-framework-sys" 1181 | version = "2.8.0" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" 1184 | dependencies = [ 1185 | "core-foundation-sys", 1186 | "libc", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "serde" 1191 | version = "1.0.193" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 1194 | dependencies = [ 1195 | "serde_derive", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "serde_derive" 1200 | version = "1.0.193" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 1203 | dependencies = [ 1204 | "proc-macro2", 1205 | "quote", 1206 | "syn 2.0.28", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "serde_json" 1211 | version = "1.0.108" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 1214 | dependencies = [ 1215 | "itoa", 1216 | "ryu", 1217 | "serde", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "sha1" 1222 | version = "0.10.1" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" 1225 | dependencies = [ 1226 | "cfg-if", 1227 | "cpufeatures", 1228 | "digest", 1229 | ] 1230 | 1231 | [[package]] 1232 | name = "sha2" 1233 | version = "0.10.2" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" 1236 | dependencies = [ 1237 | "cfg-if", 1238 | "cpufeatures", 1239 | "digest", 1240 | ] 1241 | 1242 | [[package]] 1243 | name = "shell-words" 1244 | version = "1.1.0" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 1247 | 1248 | [[package]] 1249 | name = "spectral" 1250 | version = "0.6.0" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "ae3c15181f4b14e52eeaac3efaeec4d2764716ce9c86da0c934c3e318649c5ba" 1253 | dependencies = [ 1254 | "num", 1255 | ] 1256 | 1257 | [[package]] 1258 | name = "spin" 1259 | version = "0.5.2" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1262 | 1263 | [[package]] 1264 | name = "spin" 1265 | version = "0.9.8" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1268 | 1269 | [[package]] 1270 | name = "strsim" 1271 | version = "0.10.0" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1274 | 1275 | [[package]] 1276 | name = "subtle" 1277 | version = "2.4.1" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" 1280 | 1281 | [[package]] 1282 | name = "syn" 1283 | version = "1.0.105" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 1286 | dependencies = [ 1287 | "proc-macro2", 1288 | "quote", 1289 | "unicode-ident", 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "syn" 1294 | version = "2.0.28" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" 1297 | dependencies = [ 1298 | "proc-macro2", 1299 | "quote", 1300 | "unicode-ident", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "tar" 1305 | version = "0.4.40" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" 1308 | dependencies = [ 1309 | "filetime", 1310 | "libc", 1311 | "xattr", 1312 | ] 1313 | 1314 | [[package]] 1315 | name = "tempfile" 1316 | version = "3.2.0" 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" 1318 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 1319 | dependencies = [ 1320 | "cfg-if", 1321 | "libc", 1322 | "rand 0.8.4", 1323 | "redox_syscall", 1324 | "remove_dir_all", 1325 | "winapi", 1326 | ] 1327 | 1328 | [[package]] 1329 | name = "terminal_size" 1330 | version = "0.1.17" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 1333 | dependencies = [ 1334 | "libc", 1335 | "winapi", 1336 | ] 1337 | 1338 | [[package]] 1339 | name = "termtree" 1340 | version = "0.2.1" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "78fbf2dd23e79c28ccfa2472d3e6b3b189866ffef1aeb91f17c2d968b6586378" 1343 | 1344 | [[package]] 1345 | name = "thiserror" 1346 | version = "1.0.48" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" 1349 | dependencies = [ 1350 | "thiserror-impl", 1351 | ] 1352 | 1353 | [[package]] 1354 | name = "thiserror-impl" 1355 | version = "1.0.48" 1356 | source = "registry+https://github.com/rust-lang/crates.io-index" 1357 | checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" 1358 | dependencies = [ 1359 | "proc-macro2", 1360 | "quote", 1361 | "syn 2.0.28", 1362 | ] 1363 | 1364 | [[package]] 1365 | name = "thread_local" 1366 | version = "1.1.4" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 1369 | dependencies = [ 1370 | "once_cell", 1371 | ] 1372 | 1373 | [[package]] 1374 | name = "time" 1375 | version = "0.3.10" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "82501a4c1c0330d640a6e176a3d6a204f5ec5237aca029029d21864a902e27b0" 1378 | dependencies = [ 1379 | "libc", 1380 | "num_threads", 1381 | ] 1382 | 1383 | [[package]] 1384 | name = "tinyvec" 1385 | version = "1.5.0" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" 1388 | dependencies = [ 1389 | "tinyvec_macros", 1390 | ] 1391 | 1392 | [[package]] 1393 | name = "tinyvec_macros" 1394 | version = "0.1.0" 1395 | source = "registry+https://github.com/rust-lang/crates.io-index" 1396 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1397 | 1398 | [[package]] 1399 | name = "typenum" 1400 | version = "1.15.0" 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" 1402 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 1403 | 1404 | [[package]] 1405 | name = "unicode-bidi" 1406 | version = "0.3.7" 1407 | source = "registry+https://github.com/rust-lang/crates.io-index" 1408 | checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" 1409 | 1410 | [[package]] 1411 | name = "unicode-ident" 1412 | version = "1.0.1" 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" 1414 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 1415 | 1416 | [[package]] 1417 | name = "unicode-normalization" 1418 | version = "0.1.19" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 1421 | dependencies = [ 1422 | "tinyvec", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "unicode-width" 1427 | version = "0.1.9" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 1430 | 1431 | [[package]] 1432 | name = "untrusted" 1433 | version = "0.7.1" 1434 | source = "registry+https://github.com/rust-lang/crates.io-index" 1435 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1436 | 1437 | [[package]] 1438 | name = "untrusted" 1439 | version = "0.9.0" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1442 | 1443 | [[package]] 1444 | name = "ureq" 1445 | version = "2.9.1" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" 1448 | dependencies = [ 1449 | "base64", 1450 | "flate2", 1451 | "log", 1452 | "once_cell", 1453 | "rustls", 1454 | "rustls-native-certs", 1455 | "rustls-webpki", 1456 | "serde", 1457 | "serde_json", 1458 | "url", 1459 | "webpki-roots", 1460 | ] 1461 | 1462 | [[package]] 1463 | name = "url" 1464 | version = "2.2.2" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1467 | dependencies = [ 1468 | "form_urlencoded", 1469 | "idna", 1470 | "matches", 1471 | "percent-encoding", 1472 | ] 1473 | 1474 | [[package]] 1475 | name = "utf8parse" 1476 | version = "0.2.1" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1479 | 1480 | [[package]] 1481 | name = "version_check" 1482 | version = "0.9.3" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 1485 | 1486 | [[package]] 1487 | name = "wait-timeout" 1488 | version = "0.2.0" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1491 | dependencies = [ 1492 | "libc", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "walkdir" 1497 | version = "2.3.2" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1500 | dependencies = [ 1501 | "same-file", 1502 | "winapi", 1503 | "winapi-util", 1504 | ] 1505 | 1506 | [[package]] 1507 | name = "wasi" 1508 | version = "0.11.0+wasi-snapshot-preview1" 1509 | source = "registry+https://github.com/rust-lang/crates.io-index" 1510 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1511 | 1512 | [[package]] 1513 | name = "wasm-bindgen" 1514 | version = "0.2.78" 1515 | source = "registry+https://github.com/rust-lang/crates.io-index" 1516 | checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" 1517 | dependencies = [ 1518 | "cfg-if", 1519 | "wasm-bindgen-macro", 1520 | ] 1521 | 1522 | [[package]] 1523 | name = "wasm-bindgen-backend" 1524 | version = "0.2.78" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" 1527 | dependencies = [ 1528 | "bumpalo", 1529 | "lazy_static", 1530 | "log", 1531 | "proc-macro2", 1532 | "quote", 1533 | "syn 1.0.105", 1534 | "wasm-bindgen-shared", 1535 | ] 1536 | 1537 | [[package]] 1538 | name = "wasm-bindgen-macro" 1539 | version = "0.2.78" 1540 | source = "registry+https://github.com/rust-lang/crates.io-index" 1541 | checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" 1542 | dependencies = [ 1543 | "quote", 1544 | "wasm-bindgen-macro-support", 1545 | ] 1546 | 1547 | [[package]] 1548 | name = "wasm-bindgen-macro-support" 1549 | version = "0.2.78" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" 1552 | dependencies = [ 1553 | "proc-macro2", 1554 | "quote", 1555 | "syn 1.0.105", 1556 | "wasm-bindgen-backend", 1557 | "wasm-bindgen-shared", 1558 | ] 1559 | 1560 | [[package]] 1561 | name = "wasm-bindgen-shared" 1562 | version = "0.2.78" 1563 | source = "registry+https://github.com/rust-lang/crates.io-index" 1564 | checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" 1565 | 1566 | [[package]] 1567 | name = "web-sys" 1568 | version = "0.3.55" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" 1571 | dependencies = [ 1572 | "js-sys", 1573 | "wasm-bindgen", 1574 | ] 1575 | 1576 | [[package]] 1577 | name = "webpki-roots" 1578 | version = "0.25.2" 1579 | source = "registry+https://github.com/rust-lang/crates.io-index" 1580 | checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" 1581 | 1582 | [[package]] 1583 | name = "winapi" 1584 | version = "0.3.9" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1587 | dependencies = [ 1588 | "winapi-i686-pc-windows-gnu", 1589 | "winapi-x86_64-pc-windows-gnu", 1590 | ] 1591 | 1592 | [[package]] 1593 | name = "winapi-i686-pc-windows-gnu" 1594 | version = "0.4.0" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1597 | 1598 | [[package]] 1599 | name = "winapi-util" 1600 | version = "0.1.5" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1603 | dependencies = [ 1604 | "winapi", 1605 | ] 1606 | 1607 | [[package]] 1608 | name = "winapi-x86_64-pc-windows-gnu" 1609 | version = "0.4.0" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1612 | 1613 | [[package]] 1614 | name = "windows-sys" 1615 | version = "0.42.0" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1618 | dependencies = [ 1619 | "windows_aarch64_gnullvm 0.42.2", 1620 | "windows_aarch64_msvc 0.42.2", 1621 | "windows_i686_gnu 0.42.2", 1622 | "windows_i686_msvc 0.42.2", 1623 | "windows_x86_64_gnu 0.42.2", 1624 | "windows_x86_64_gnullvm 0.42.2", 1625 | "windows_x86_64_msvc 0.42.2", 1626 | ] 1627 | 1628 | [[package]] 1629 | name = "windows-sys" 1630 | version = "0.48.0" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1633 | dependencies = [ 1634 | "windows-targets", 1635 | ] 1636 | 1637 | [[package]] 1638 | name = "windows-targets" 1639 | version = "0.48.0" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 1642 | dependencies = [ 1643 | "windows_aarch64_gnullvm 0.48.0", 1644 | "windows_aarch64_msvc 0.48.0", 1645 | "windows_i686_gnu 0.48.0", 1646 | "windows_i686_msvc 0.48.0", 1647 | "windows_x86_64_gnu 0.48.0", 1648 | "windows_x86_64_gnullvm 0.48.0", 1649 | "windows_x86_64_msvc 0.48.0", 1650 | ] 1651 | 1652 | [[package]] 1653 | name = "windows_aarch64_gnullvm" 1654 | version = "0.42.2" 1655 | source = "registry+https://github.com/rust-lang/crates.io-index" 1656 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1657 | 1658 | [[package]] 1659 | name = "windows_aarch64_gnullvm" 1660 | version = "0.48.0" 1661 | source = "registry+https://github.com/rust-lang/crates.io-index" 1662 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 1663 | 1664 | [[package]] 1665 | name = "windows_aarch64_msvc" 1666 | version = "0.42.2" 1667 | source = "registry+https://github.com/rust-lang/crates.io-index" 1668 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1669 | 1670 | [[package]] 1671 | name = "windows_aarch64_msvc" 1672 | version = "0.48.0" 1673 | source = "registry+https://github.com/rust-lang/crates.io-index" 1674 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1675 | 1676 | [[package]] 1677 | name = "windows_i686_gnu" 1678 | version = "0.42.2" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1681 | 1682 | [[package]] 1683 | name = "windows_i686_gnu" 1684 | version = "0.48.0" 1685 | source = "registry+https://github.com/rust-lang/crates.io-index" 1686 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1687 | 1688 | [[package]] 1689 | name = "windows_i686_msvc" 1690 | version = "0.42.2" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1693 | 1694 | [[package]] 1695 | name = "windows_i686_msvc" 1696 | version = "0.48.0" 1697 | source = "registry+https://github.com/rust-lang/crates.io-index" 1698 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1699 | 1700 | [[package]] 1701 | name = "windows_x86_64_gnu" 1702 | version = "0.42.2" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1705 | 1706 | [[package]] 1707 | name = "windows_x86_64_gnu" 1708 | version = "0.48.0" 1709 | source = "registry+https://github.com/rust-lang/crates.io-index" 1710 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1711 | 1712 | [[package]] 1713 | name = "windows_x86_64_gnullvm" 1714 | version = "0.42.2" 1715 | source = "registry+https://github.com/rust-lang/crates.io-index" 1716 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1717 | 1718 | [[package]] 1719 | name = "windows_x86_64_gnullvm" 1720 | version = "0.48.0" 1721 | source = "registry+https://github.com/rust-lang/crates.io-index" 1722 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1723 | 1724 | [[package]] 1725 | name = "windows_x86_64_msvc" 1726 | version = "0.42.2" 1727 | source = "registry+https://github.com/rust-lang/crates.io-index" 1728 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1729 | 1730 | [[package]] 1731 | name = "windows_x86_64_msvc" 1732 | version = "0.48.0" 1733 | source = "registry+https://github.com/rust-lang/crates.io-index" 1734 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1735 | 1736 | [[package]] 1737 | name = "xattr" 1738 | version = "1.0.1" 1739 | source = "registry+https://github.com/rust-lang/crates.io-index" 1740 | checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" 1741 | dependencies = [ 1742 | "libc", 1743 | ] 1744 | 1745 | [[package]] 1746 | name = "zeroize" 1747 | version = "1.4.2" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970" 1750 | 1751 | [[package]] 1752 | name = "zip" 1753 | version = "0.6.6" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" 1756 | dependencies = [ 1757 | "aes", 1758 | "byteorder", 1759 | "bzip2", 1760 | "constant_time_eq", 1761 | "crc32fast", 1762 | "crossbeam-utils", 1763 | "flate2", 1764 | "hmac", 1765 | "pbkdf2", 1766 | "sha1", 1767 | "time", 1768 | "zstd", 1769 | ] 1770 | 1771 | [[package]] 1772 | name = "zstd" 1773 | version = "0.11.2+zstd.1.5.2" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" 1776 | dependencies = [ 1777 | "zstd-safe", 1778 | ] 1779 | 1780 | [[package]] 1781 | name = "zstd-safe" 1782 | version = "5.0.2+zstd.1.5.2" 1783 | source = "registry+https://github.com/rust-lang/crates.io-index" 1784 | checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" 1785 | dependencies = [ 1786 | "libc", 1787 | "zstd-sys", 1788 | ] 1789 | 1790 | [[package]] 1791 | name = "zstd-sys" 1792 | version = "2.0.1+zstd.1.5.2" 1793 | source = "registry+https://github.com/rust-lang/crates.io-index" 1794 | checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" 1795 | dependencies = [ 1796 | "cc", 1797 | "libc", 1798 | ] 1799 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nvm-rust" 3 | version = "0.4.3" 4 | description = "A node version manager that doesn't suck" 5 | authors = ["BeeeQueue "] 6 | repository = "https://github.com/BeeeQueue/nvm-rust" 7 | homepage = "https://github.com/BeeeQueue/nvm-rust" 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | keywords = ["node", "version", "manager", "nvm"] 11 | categories = ["command-line-utilities"] 12 | edition = "2021" 13 | 14 | exclude = [ 15 | ".github/", 16 | "tests/", 17 | "test-data/", 18 | ] 19 | 20 | [profile.release] 21 | strip = true 22 | lto = true 23 | 24 | [[bin]] 25 | name = "nvm" 26 | path = "src/main.rs" 27 | 28 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 29 | 30 | [dependencies] 31 | anyhow = "1.0.75" 32 | clap = { version = "4.4.11", features = ["derive", "env", "cargo"] } 33 | dialoguer = "0.11.0" 34 | dirs = "4.0.0" 35 | itertools = "0.12.0" 36 | node-semver = "2.1.0" 37 | serde = { version = "1.0.193", features = ["derive"] } 38 | serde_json = "1.0.108" 39 | spectral = "0.6.0" 40 | ureq = { version = "2.9.1", features = ["native-certs", "json"] } 41 | 42 | [target.'cfg(unix)'.dependencies] 43 | flate2 = "1.0.28" 44 | tar = "0.4.40" 45 | 46 | [target.'cfg(windows)'.dependencies] 47 | zip = "0.6.6" 48 | 49 | [dev-dependencies] 50 | assert_cmd = "2.0.12" 51 | assert_fs = "1.0.13" 52 | predicates = "3.0.4" 53 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2022 Adam Haglund 2 | Copyright (c) 2014 The Rust Project Developers 3 | 4 | Permission is hereby granted, free of charge, to any 5 | person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the 7 | Software without restriction, including without 8 | limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software 11 | is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice 15 | shall be included in all copies or substantial portions 16 | of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 19 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 20 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 21 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 22 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 25 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvm(-rust) 2 | 3 | Cross platform nvm that doesn't suck™ 4 | 5 | ## Installation 6 | 7 | ### Binaries 8 | 9 | 1. Download binary for your OS from the [Releases](https://github.com/BeeeQueue/nvm-rust/releases) 10 | 2. Rename the file to `nvm` and place it somewhere in your `$PATH` 11 | 3. Add `path/to/nvm-home/shims` to your PATH _(TODO: document this)_ 12 | 4. Enjoy? 13 | 14 | ### Cargo 15 | 16 | ```shell 17 | cargo install nvm-rust 18 | ``` 19 | 20 | #### Note for Windows 21 | 22 | _It does not allow creating the symlinks this program uses without either Admin access or Developer Mode._ 23 | 24 | _Either run the program as Administrator or [enable Developer Mode](https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development#active-developer-mode)_ 25 | 26 | _[Read more about it here](https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10)_ 27 | 28 | ## Feature Comparison 29 | 30 | | | **nvm-rust** | [nvm-windows](https://github.com/coreybutler/nvm-windows) | [nvm](https://github.com/nvm-sh/nvm) | 31 | |-----------------------------------------------------------------------:|:---------------:|:---------------------------------------------------------:|:------------------------------------:| 32 | | Platforms | Win, Mac, Linux | Windows | POSIX | 33 | | [Range matching](#range-matching) | ✅ | ❌ | ✅ | 34 | | [Version files](#version-files-packagejsonengines-nvmrc-tool-versions) | ✅ | ❌ | ✅ | 35 | | [Default global packages](#default-global-packages) | ❌ | ❌ | ✅ | 36 | | Node <4 | ✅* | ✅ | ✅ | 37 | | Disabling nvm temporarily | ❌ | ✅ | ✅ | 38 | | Caching | ❌ | ❌ | ✅ | 39 | | Aliases | ❌ | ❌ | ✅ | 40 | 41 | **not supported, might work? 42 | 43 | ### Range Matching 44 | 45 | Allowing you to not have to write out the full versions when running a command. 46 | 47 | For example: 48 | 49 | - `nvm install 12` will install the latest version matching `12`, instead of `12.0.0`. 50 | - `nvm install "12 <12.18"` will install the latest `12.17.x` version, instead of just giving you an error. 51 | - `nvm use 12` switch use the newest installed `12.x.x` version instead of `12.0.0` (and most likely giving you an error, who has that version installed?). 52 | 53 | ### Version files (`package.json#engines`, `.nvmrc`, `.tool-versions`) 54 | 55 | If a version is not specified for the `use` and `install` commands nvm-rust will look for and parse any files containing Node version specifications amd use that! 56 | 57 | nvm-rust handles files containing ranges, unlike [nvm](https://github.com/nvm-sh/nvm). 58 | 59 | e.g. 60 | 61 | ``` 62 | // package.json 63 | { 64 | ... 65 | "engines": { 66 | "node": "^14.17" 67 | } 68 | ... 69 | } 70 | 71 | # Installs 14.19.3 as of the time of writing 72 | $ nvm install 73 | ``` 74 | 75 | The program will use the following file priority: 76 | 77 | 1. `package.json#engines` 78 | 2. `.nvmrc` 79 | 3. `.node-version` 80 | 4. [`.tool-versions` from `asdf`](https://asdf-vm.com/guide/getting-started.html#local) 81 | 82 | ### Default global packages 83 | 84 | 85 | ## Development 86 | 87 | This project uses [Task](https://taskfile.dev/installation) to execute various development commands. 88 | 89 | e.g. to run a command via a debug build, run: 90 | 91 | ```shell 92 | task run -- install 12 93 | ``` 94 | 95 | To build a release artifact, run: 96 | 97 | ```shell 98 | task build:release 99 | ``` 100 | 101 | You can find all the commands in the [Taskfile](./Taskfile.yml). 102 | 103 | ## Publish new version 104 | 105 | 1. Up version number in `Cargo.toml` 106 | 2. Create tag on commit updating the version with said version (`vX.X.X`) 107 | 3. Push both 108 | 4. Wait for CI to create draft release for tag 109 | 5. Edit draft release notes 110 | 6. Publish 111 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: 3 2 | 3 | tasks: 4 | lint: 5 | desc: Lint code 6 | cmds: 7 | - cargo clippy {{.CLI_ARGS}} 8 | - cargo fmt --check --all 9 | lint:fix: 10 | desc: Lint code and fix problems with autofixes 11 | cmds: 12 | - cargo clippy --fix --allow-staged --allow-dirty 13 | - task: format 14 | 15 | format: 16 | desc: Format code 17 | cmds: 18 | - cargo fmt --all {{.CLI_ARGS}} 19 | 20 | test: 21 | desc: Run tests 22 | sources: 23 | - Cargo.* 24 | - src/** 25 | - test-data/** 26 | - tests/** 27 | cmds: 28 | - cargo test {{.CLI_ARGS}} 29 | 30 | run: 31 | desc: "Run the CLI with a debug build: task run -- <...args>" 32 | cmds: 33 | - cargo run {{.CLI_ARGS}} 34 | 35 | build: 36 | desc: Build debug artifacts 37 | sources: 38 | - Cargo.* 39 | - src/** 40 | generates: 41 | - target/debug/** 42 | cmds: 43 | - cargo build {{.CLI_ARGS}} 44 | 45 | build:release: 46 | desc: Build release artifacts 47 | sources: 48 | - Cargo.* 49 | - src/** 50 | generates: 51 | - target/release/** 52 | cmds: 53 | - cargo build --release --locked {{.CLI_ARGS}} 54 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Unix" 2 | use_field_init_shorthand = true 3 | 4 | ### Mine 5 | trailing_comma = "Vertical" 6 | match_block_trailing_comma = true 7 | 8 | imports_granularity = "Crate" 9 | -------------------------------------------------------------------------------- /src/archives.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use std::fs::remove_dir_all; 3 | #[cfg(windows)] 4 | use std::fs::File; 5 | #[cfg(windows)] 6 | use std::io::copy; 7 | #[cfg(unix)] 8 | use std::path::PathBuf; 9 | use std::{fs::create_dir_all, io::Cursor, path::Path}; 10 | 11 | use anyhow::Result; 12 | #[cfg(unix)] 13 | use flate2::read::GzDecoder; 14 | #[cfg(unix)] 15 | use tar::{Archive, Unpacked}; 16 | #[cfg(target_os = "windows")] 17 | use zip::ZipArchive; 18 | 19 | #[cfg(target_os = "windows")] 20 | pub fn extract_archive(bytes: Vec, path: &Path) -> Result<()> { 21 | let reader = Cursor::new(bytes); 22 | let mut archive = ZipArchive::new(reader).unwrap(); 23 | 24 | println!("Extracting..."); 25 | 26 | for i in 0..archive.len() { 27 | let mut item = archive.by_index(i).unwrap(); 28 | let file_path = item.mangled_name(); 29 | let file_path = file_path.to_string_lossy(); 30 | 31 | let mut new_path = path.to_owned(); 32 | if let Some(index) = file_path.find('\\') { 33 | new_path.push(&file_path[index + 1..]); 34 | } 35 | 36 | if item.is_dir() && !new_path.exists() { 37 | create_dir_all(&new_path) 38 | .unwrap_or_else(|_| panic!("Could not create new folder: {new_path:?}")); 39 | } 40 | 41 | if item.is_file() { 42 | let mut file = File::create(&*new_path)?; 43 | copy(&mut item, &mut file).unwrap_or_else(|_| panic!("Couldn't write to {new_path:?}")); 44 | } 45 | } 46 | 47 | println!( 48 | "Extracted to {}", 49 | // Have to remove \\?\ prefix 🤮 50 | path.to_str() 51 | .unwrap() 52 | .strip_prefix("\\\\?\\") 53 | .unwrap_or_else(|| path.to_str().unwrap()) 54 | ); 55 | 56 | Ok(()) 57 | } 58 | 59 | #[cfg(unix)] 60 | pub fn extract_archive(bytes: Vec, path: &Path) -> Result<()> { 61 | let reader = Cursor::new(bytes); 62 | let tar = GzDecoder::new(reader); 63 | let mut archive = Archive::new(tar); 64 | 65 | let version_dir_path = path.to_owned(); 66 | create_dir_all(&version_dir_path).expect("fuck"); 67 | 68 | println!("Extracting..."); 69 | 70 | let result = archive 71 | .entries() 72 | .map_err(anyhow::Error::from)? 73 | .filter_map(|e| e.ok()) 74 | .map(|mut entry| -> Result { 75 | let file_path = entry.path()?; 76 | let file_path = file_path.to_str().unwrap(); 77 | 78 | let new_path: PathBuf = if let Some(index) = file_path.find('/') { 79 | path.to_owned().join(&file_path[index + 1..]) 80 | } else { 81 | // This happens if it's the root index, the base folder 82 | path.to_owned() 83 | }; 84 | 85 | entry.set_preserve_permissions(false); 86 | entry.unpack(&new_path).map_err(anyhow::Error::from) 87 | }); 88 | 89 | let errors: Vec = result 90 | .into_iter() 91 | .filter(|result| result.is_err()) 92 | .map(|result| result.unwrap_err()) 93 | .collect(); 94 | 95 | if !errors.is_empty() { 96 | remove_dir_all(version_dir_path).expect("Couldn't clean up version."); 97 | 98 | return Err(anyhow::anyhow!( 99 | "Failed to extract all files:\n{:?}", 100 | errors 101 | .into_iter() 102 | .map(|err| err.to_string()) 103 | .collect::>() 104 | .join("/n") 105 | )); 106 | } 107 | 108 | println!("Extracted to {version_dir_path:?}"); 109 | 110 | Ok(()) 111 | } 112 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | pub const EXEC_EXT: &str = ".cmd"; 3 | #[cfg(not(windows))] 4 | pub const EXEC_EXT: &str = ""; 5 | 6 | #[cfg(target_os = "windows")] 7 | pub const PLATFORM: &str = "win"; 8 | #[cfg(target_os = "macos")] 9 | pub const PLATFORM: &str = "darwin"; 10 | #[cfg(target_os = "linux")] 11 | pub const PLATFORM: &str = "linux"; 12 | 13 | #[cfg(target_os = "windows")] 14 | pub const EXT: &str = ".zip"; 15 | #[cfg(target_os = "macos")] 16 | pub const EXT: &str = ".tar.gz"; 17 | #[cfg(target_os = "linux")] 18 | pub const EXT: &str = ".tar.gz"; 19 | 20 | #[cfg(target_arch = "x86_64")] 21 | pub const ARCH: &str = "x64"; 22 | #[cfg(target_arch = "x86")] 23 | pub const ARCH: &str = "x86"; 24 | #[cfg(target_arch = "aarch64")] 25 | pub const ARCH: &str = "arm64"; 26 | 27 | pub const X64: &str = "x64"; 28 | -------------------------------------------------------------------------------- /src/files/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use itertools::Itertools; 4 | use node_semver::Range; 5 | 6 | pub mod package_json; 7 | 8 | const PACKAGE_JSON_FILE_NAME: &str = "package.json"; 9 | const NVMRC_FILE_NAME: &str = ".nvmrc"; 10 | const NODE_VERSION_FILE_NAME: &str = ".node-version"; 11 | const ASDF_FILE_NAME: &str = ".tool-versions"; 12 | 13 | pub enum VersionFile { 14 | Nvmrc(Range), 15 | PackageJson(Range), 16 | Asdf(Range), 17 | } 18 | 19 | impl VersionFile { 20 | pub fn range(self) -> Range { 21 | match self { 22 | VersionFile::Nvmrc(range) => range, 23 | VersionFile::PackageJson(range) => range, 24 | VersionFile::Asdf(range) => range, 25 | } 26 | } 27 | } 28 | 29 | pub fn get_version_file() -> Option { 30 | if PathBuf::from(PACKAGE_JSON_FILE_NAME).exists() { 31 | let parse_result = 32 | package_json::PackageJson::try_from(PathBuf::from(PACKAGE_JSON_FILE_NAME)); 33 | 34 | if let Ok(parse_result) = parse_result { 35 | return parse_result 36 | .engines 37 | .and_then(|engines| engines.node) 38 | .map(VersionFile::PackageJson); 39 | } else { 40 | println!( 41 | "Failed to parse package.json: {}", 42 | parse_result.unwrap_err() 43 | ); 44 | } 45 | } 46 | 47 | if let Some(existing_file) = [NVMRC_FILE_NAME, NODE_VERSION_FILE_NAME] 48 | .iter() 49 | .find_or_first(|&path| PathBuf::from(path).exists()) 50 | { 51 | let contents = fs::read_to_string(existing_file); 52 | 53 | if let Ok(contents) = contents { 54 | let parse_result = Range::parse(contents.trim()); 55 | 56 | if let Ok(parse_result) = parse_result { 57 | return Some(VersionFile::Nvmrc(parse_result)); 58 | } else { 59 | println!( 60 | "Failed to parse {}: '{}'", 61 | existing_file, 62 | parse_result.unwrap_err().input(), 63 | ); 64 | } 65 | } 66 | } 67 | 68 | if PathBuf::from(ASDF_FILE_NAME).exists() { 69 | let contents = fs::read_to_string(ASDF_FILE_NAME); 70 | 71 | if let Ok(contents) = contents { 72 | let version_string = contents 73 | .lines() 74 | .find(|line| line.starts_with("nodejs")) 75 | .and_then(|line| line.split(' ').nth(1)); 76 | 77 | if let Some(version_string) = version_string { 78 | let parse_result = Range::parse(version_string); 79 | 80 | if let Ok(parse_result) = parse_result { 81 | return Some(VersionFile::Asdf(parse_result)); 82 | } else { 83 | println!( 84 | "Failed to parse {}: '{}'", 85 | ASDF_FILE_NAME, 86 | parse_result.unwrap_err().input(), 87 | ); 88 | } 89 | } 90 | } 91 | } 92 | 93 | None 94 | } 95 | -------------------------------------------------------------------------------- /src/files/package_json.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use node_semver::Range; 4 | use serde::Deserialize; 5 | 6 | #[derive(Clone, Deserialize, Debug, Eq, PartialEq)] 7 | pub struct PackageJson { 8 | #[serde()] 9 | pub name: Option, 10 | #[serde()] 11 | pub version: Option, 12 | #[serde()] 13 | pub engines: Option, 14 | } 15 | 16 | #[derive(Clone, Deserialize, Debug, Eq, PartialEq)] 17 | pub struct PackageJsonEngines { 18 | #[serde()] 19 | pub node: Option, 20 | } 21 | 22 | impl TryFrom for PackageJson { 23 | type Error = anyhow::Error; 24 | 25 | fn try_from(path: PathBuf) -> Result { 26 | let contents = fs::read_to_string(path)?; 27 | let package_json: PackageJson = serde_json::from_str(&contents)?; 28 | 29 | Ok(package_json) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | use std::os::windows; 3 | use std::{ 4 | fs, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | #[cfg(windows)] 9 | use anyhow::bail; 10 | use anyhow::Result; 11 | use clap::{Parser, ValueHint}; 12 | 13 | use crate::subcommand::{ 14 | install::InstallCommand, is_installed::IsInstalledCommand, list::ListCommand, 15 | parse_version::ParseVersionCommand, switch::SwitchCommand, uninstall::UninstallCommand, Action, 16 | }; 17 | 18 | mod archives; 19 | mod constants; 20 | mod files; 21 | mod node_version; 22 | mod subcommand; 23 | 24 | #[derive(Parser, Clone, Debug)] 25 | enum Subcommands { 26 | List(ListCommand), 27 | IsInstalled(IsInstalledCommand), 28 | Install(InstallCommand), 29 | Uninstall(UninstallCommand), 30 | Use(SwitchCommand), 31 | ParseVersion(ParseVersionCommand), 32 | } 33 | 34 | #[derive(Parser, Debug)] 35 | #[command( 36 | name = "nvm(-rust)", 37 | author, 38 | about, 39 | version, 40 | about = "Node Version Manager (but better, and in Rust)" 41 | )] 42 | pub struct Config { 43 | /// Installation directory 44 | #[arg( 45 | id("install-dir"), 46 | global(true), 47 | long, 48 | value_hint(ValueHint::DirPath), 49 | env("NVM_DIR") 50 | )] 51 | dir: Option, 52 | /// bin directory 53 | #[arg( 54 | global(true), 55 | long, 56 | value_hint(ValueHint::DirPath), 57 | env("NVM_SHIMS_DIR") 58 | )] 59 | shims_dir: Option, 60 | /// Accept any prompts needed for the command to complete 61 | #[arg(global(true), short, long)] 62 | force: bool, 63 | 64 | #[command(subcommand)] 65 | command: Subcommands, 66 | } 67 | 68 | impl Config { 69 | pub fn get_dir(&self) -> PathBuf { 70 | self.dir 71 | .as_ref() 72 | .map_or_else(Config::default_dir, |r| r.clone()) 73 | } 74 | 75 | pub fn get_shims_dir(&self) -> PathBuf { 76 | self.shims_dir 77 | .as_ref() 78 | .map_or_else(|| self.get_dir().join("shims"), |r| r.clone()) 79 | } 80 | 81 | /// Path to directory containing node versions 82 | fn get_versions_dir(&self) -> PathBuf { 83 | self.get_dir().join("versions") 84 | } 85 | 86 | fn with_force(&self) -> Self { 87 | Self { 88 | force: true, 89 | dir: Some(self.get_dir()), 90 | shims_dir: Some(self.get_shims_dir()), 91 | command: self.command.clone(), 92 | } 93 | } 94 | 95 | #[cfg(windows)] 96 | fn default_dir() -> PathBuf { 97 | dirs::data_local_dir().unwrap().join("nvm-rust") 98 | } 99 | 100 | #[cfg(unix)] 101 | fn default_dir() -> PathBuf { 102 | dirs::home_dir().unwrap().join("nvm-rust") 103 | } 104 | } 105 | 106 | fn ensure_dir_exists(path: &Path) { 107 | if !path.exists() { 108 | fs::create_dir_all(path).unwrap_or_else(|err| panic!("Could not create {path:?} - {err}")); 109 | 110 | println!("Created nvm dir at {path:?}"); 111 | } 112 | 113 | if !path.is_dir() { 114 | panic!("{path:?} is not a directory! Please rename it.") 115 | } 116 | } 117 | 118 | #[cfg(windows)] 119 | const SYMLINK_ERROR: &str = "You do not seem to have permissions to create symlinks. 120 | This is most likely due to Windows requiring Admin access for it unless you enable Developer Mode. 121 | 122 | Either run the program as Administrator or enable Developer Mode: 123 | https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development#active-developer-mode 124 | 125 | Read more: 126 | https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10"; 127 | 128 | #[cfg(windows)] 129 | fn ensure_symlinks_work(config: &Config) -> Result<()> { 130 | let target_path = &config.get_dir().join("test"); 131 | 132 | if windows::fs::symlink_dir(config.get_shims_dir(), target_path).is_err() { 133 | bail!("{SYMLINK_ERROR}"); 134 | } 135 | 136 | fs::remove_dir(target_path).expect("Could not remove test symlink..."); 137 | 138 | Ok(()) 139 | } 140 | 141 | fn main() -> Result<()> { 142 | let config: Config = Config::parse(); 143 | #[cfg(windows)] 144 | let is_initial_run = !config.get_dir().exists(); 145 | 146 | ensure_dir_exists(&config.get_dir()); 147 | ensure_dir_exists(&config.get_versions_dir()); 148 | 149 | #[cfg(windows)] 150 | if is_initial_run { 151 | let result = ensure_symlinks_work(&config); 152 | result?; 153 | } 154 | 155 | match config.command { 156 | Subcommands::List(ref options) => ListCommand::run(&config, options), 157 | Subcommands::IsInstalled(ref options) => IsInstalledCommand::run(&config, options), 158 | Subcommands::Install(ref options) => InstallCommand::run(&config, options), 159 | Subcommands::Uninstall(ref options) => UninstallCommand::run(&config, options), 160 | Subcommands::Use(ref options) => SwitchCommand::run(&config, options), 161 | Subcommands::ParseVersion(ref options) => ParseVersionCommand::run(&config, options), 162 | #[allow(unreachable_patterns)] 163 | _ => Ok(()), 164 | } 165 | } 166 | 167 | #[test] 168 | fn verify_cli() { 169 | use clap::CommandFactory; 170 | 171 | Config::command().debug_assert() 172 | } 173 | -------------------------------------------------------------------------------- /src/node_version.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::Ordering, 3 | collections::HashMap, 4 | fs::{read_link, remove_dir_all}, 5 | path::PathBuf, 6 | }; 7 | 8 | use anyhow::{Context, Result}; 9 | use node_semver::{Range, Version}; 10 | use serde::Deserialize; 11 | 12 | use crate::{ 13 | constants, 14 | constants::{ARCH, EXT, PLATFORM, X64}, 15 | Config, 16 | }; 17 | 18 | pub trait NodeVersion { 19 | fn version(&self) -> &Version; 20 | } 21 | 22 | impl PartialEq for dyn NodeVersion { 23 | fn eq(&self, other: &Self) -> bool { 24 | self.version().eq(other.version()) 25 | } 26 | } 27 | 28 | impl Eq for dyn NodeVersion {} 29 | 30 | impl PartialOrd for dyn NodeVersion { 31 | fn partial_cmp(&self, other: &Self) -> Option { 32 | Some(self.version().cmp(other.version())) 33 | } 34 | } 35 | 36 | impl Ord for dyn NodeVersion { 37 | fn cmp(&self, other: &Self) -> Ordering { 38 | self.version().cmp(other.version()) 39 | } 40 | } 41 | 42 | pub fn parse_range(value: &str) -> Result { 43 | Range::parse(value).context(value.to_string()) 44 | } 45 | 46 | pub fn filter_version_req(versions: Vec, version_range: &Range) -> Vec { 47 | versions 48 | .into_iter() 49 | .filter(|version| version_range.satisfies(version.version())) 50 | .collect() 51 | } 52 | 53 | pub fn get_latest_of_each_major(versions: &[V]) -> Vec<&V> { 54 | let mut map: HashMap = HashMap::new(); 55 | 56 | for version in versions.iter() { 57 | let entry = map.get_mut(&version.version().major); 58 | if entry.is_some() && version.version().lt(entry.unwrap().version()) { 59 | continue; 60 | } 61 | 62 | map.insert(version.version().major, version); 63 | } 64 | 65 | map.values().cloned().collect() 66 | } 67 | 68 | /// Handles `vX.X.X` prefixes 69 | fn parse_version_str(version_str: &str) -> Result { 70 | // Required since the versions are prefixed with 'v' which `semver` can't handle 71 | let clean_version = if version_str.starts_with('v') { 72 | version_str.get(1..).unwrap() 73 | } else { 74 | version_str 75 | }; 76 | 77 | Version::parse(clean_version).context(version_str.to_owned()) 78 | } 79 | 80 | #[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)] 81 | #[serde(rename_all(deserialize = "snake_case"))] 82 | pub struct OnlineNodeVersion { 83 | #[serde()] 84 | version: Version, 85 | #[serde(alias = "date")] 86 | pub release_date: String, 87 | 88 | files: Vec, 89 | } 90 | 91 | impl OnlineNodeVersion { 92 | pub fn fetch_all() -> Result> { 93 | let response = ureq::get("https://nodejs.org/dist/index.json").call()?; 94 | 95 | response 96 | .into_json() 97 | .context("Failed to parse versions list from nodejs.org") 98 | } 99 | 100 | pub fn install_path(&self, config: &Config) -> PathBuf { 101 | config.get_versions_dir().join(self.to_string()) 102 | } 103 | 104 | pub fn download_url(&self) -> String { 105 | let file_name: String; 106 | 107 | #[cfg(target_os = "macos")] 108 | { 109 | let has_arm = self.has_arm(); 110 | 111 | file_name = self.file(!has_arm); 112 | } 113 | 114 | #[cfg(not(target_os = "macos"))] 115 | { 116 | file_name = self.file(false); 117 | } 118 | 119 | format!("https://nodejs.org/dist/v{}/{}", self.version, file_name) 120 | } 121 | 122 | fn file(&self, force_x64: bool) -> String { 123 | format!( 124 | "node-v{VERSION}-{PLATFORM}-{ARCH}{EXT}", 125 | VERSION = self.version(), 126 | ARCH = if force_x64 { X64 } else { ARCH }, 127 | ) 128 | } 129 | 130 | #[cfg(target_os = "macos")] 131 | fn has_arm(&self) -> bool { 132 | for file in self.files.iter() { 133 | // We check for both ARM _and_ OSX since we don't want to fall back to x64 on other platforms. 134 | if file.contains("osx") && file.contains("arm64") { 135 | return true; 136 | } 137 | } 138 | 139 | false 140 | } 141 | } 142 | 143 | impl ToString for OnlineNodeVersion { 144 | fn to_string(&self) -> String { 145 | self.version.to_string() 146 | } 147 | } 148 | 149 | impl NodeVersion for OnlineNodeVersion { 150 | fn version(&self) -> &Version { 151 | &self.version 152 | } 153 | } 154 | 155 | #[derive(Clone, Deserialize, Debug, Eq, PartialEq)] 156 | pub struct InstalledNodeVersion { 157 | version: Version, 158 | path: PathBuf, 159 | } 160 | 161 | impl InstalledNodeVersion { 162 | // Properties 163 | 164 | pub fn get_dir_path(&self, config: &Config) -> PathBuf { 165 | config.get_versions_dir().join(self.version().to_string()) 166 | } 167 | 168 | pub fn is_installed(config: &Config, version: &Version) -> bool { 169 | Self::list(config).iter().any(|v| v.version().eq(version)) 170 | } 171 | 172 | pub fn is_selected(&self, config: &Config) -> bool { 173 | let path = config.get_shims_dir(); 174 | let real_path = read_link(path); 175 | 176 | if real_path.is_err() { 177 | return false; 178 | } 179 | 180 | let real_path = real_path.unwrap(); 181 | 182 | real_path 183 | .to_string_lossy() 184 | .contains(&self.version().to_string()) 185 | } 186 | 187 | // Functions 188 | 189 | pub fn uninstall(self, config: &Config) -> Result<()> { 190 | remove_dir_all(self.get_dir_path(config))?; 191 | 192 | println!("Uninstalled {}!", self.version()); 193 | Ok(()) 194 | } 195 | 196 | /// Checks that all the required files are present in the installation dir 197 | #[allow(dead_code)] 198 | pub fn validate(&self, config: &Config) -> Result<()> { 199 | let version_dir = 200 | read_link(config.get_shims_dir()).expect("Could not read installation dir"); 201 | 202 | let mut required_files = vec![version_dir; 2]; 203 | required_files[0].set_file_name(format!("node{}", constants::EXEC_EXT)); 204 | required_files[1].set_file_name(format!("npm{}", constants::EXEC_EXT)); 205 | 206 | if let Some(missing_file) = required_files.iter().find(|file| !file.exists()) { 207 | anyhow::bail!( 208 | "{:?} is not preset for {:?}", 209 | missing_file, 210 | self.version.to_string() 211 | ); 212 | } 213 | 214 | Ok(()) 215 | } 216 | 217 | // Static functions 218 | 219 | pub fn deselect(config: &Config) -> Result<()> { 220 | remove_dir_all(config.get_shims_dir()).map_err(anyhow::Error::from) 221 | } 222 | 223 | pub fn list(config: &Config) -> Vec { 224 | let mut version_dirs: Vec = vec![]; 225 | 226 | for entry in config 227 | .get_versions_dir() 228 | .read_dir() 229 | .expect("Failed to read nvm dir") 230 | { 231 | if entry.is_err() { 232 | println!("Could not read {entry:?}"); 233 | continue; 234 | } 235 | 236 | let entry = entry.unwrap(); 237 | let result = parse_version_str(entry.file_name().to_string_lossy().as_ref()); 238 | 239 | if let Ok(version) = result { 240 | version_dirs.push(version); 241 | } 242 | } 243 | 244 | version_dirs.sort(); 245 | version_dirs.reverse(); 246 | 247 | version_dirs 248 | .iter() 249 | .map(|version| { 250 | let version_str = version.to_string(); 251 | 252 | InstalledNodeVersion { 253 | version: parse_version_str(&version_str) 254 | .expect("Got bad version into InstalledNodeVersion."), 255 | path: config.get_versions_dir().join(&version_str), 256 | } 257 | }) 258 | .collect() 259 | } 260 | 261 | /// Returns the latest, installed version matching the version range 262 | pub fn find_matching(config: &Config, range: &Range) -> Option { 263 | Self::list(config) 264 | .iter() 265 | .find(|inv| range.satisfies(inv.version())) 266 | .map(|inv| inv.to_owned()) 267 | } 268 | } 269 | 270 | impl ToString for InstalledNodeVersion { 271 | fn to_string(&self) -> String { 272 | self.version.to_string() 273 | } 274 | } 275 | 276 | impl NodeVersion for InstalledNodeVersion { 277 | fn version(&self) -> &Version { 278 | &self.version 279 | } 280 | } 281 | 282 | #[cfg(test)] 283 | mod tests { 284 | mod online_version { 285 | use anyhow::Result; 286 | use node_semver::Version; 287 | 288 | use spectral::prelude::*; 289 | 290 | use crate::node_version::OnlineNodeVersion; 291 | 292 | #[test] 293 | fn can_parse_version_data() -> Result<()> { 294 | let expected = OnlineNodeVersion { 295 | version: Version { 296 | major: 14, 297 | minor: 18, 298 | patch: 0, 299 | build: vec![], 300 | pre_release: vec![], 301 | }, 302 | release_date: "2021-09-28".to_string(), 303 | files: vec![ 304 | "aix-ppc64".to_string(), 305 | "headers".to_string(), 306 | "linux-arm64".to_string(), 307 | "linux-armv7l".to_string(), 308 | "linux-ppc64le".to_string(), 309 | "linux-s390x".to_string(), 310 | "linux-x64".to_string(), 311 | "osx-x64-pkg".to_string(), 312 | "osx-x64-tar".to_string(), 313 | "src".to_string(), 314 | "win-x64-7z".to_string(), 315 | "win-x64-exe".to_string(), 316 | "win-x64-msi".to_string(), 317 | "win-x64-zip".to_string(), 318 | "win-x86-7z".to_string(), 319 | "win-x86-exe".to_string(), 320 | "win-x86-msi".to_string(), 321 | "win-x86-zip".to_string(), 322 | ], 323 | }; 324 | 325 | let json_str = r#" 326 | { 327 | "version": "v14.18.0", 328 | "date": "2021-09-28", 329 | "files": [ 330 | "aix-ppc64", 331 | "headers", 332 | "linux-arm64", 333 | "linux-armv7l", 334 | "linux-ppc64le", 335 | "linux-s390x", 336 | "linux-x64", 337 | "osx-x64-pkg", 338 | "osx-x64-tar", 339 | "src", 340 | "win-x64-7z", 341 | "win-x64-exe", 342 | "win-x64-msi", 343 | "win-x64-zip", 344 | "win-x86-7z", 345 | "win-x86-exe", 346 | "win-x86-msi", 347 | "win-x86-zip" 348 | ], 349 | "npm": "6.14.15", 350 | "v8": "8.4.371.23", 351 | "uv": "1.42.0", 352 | "zlib": "1.2.11", 353 | "openssl": "1.1.1l", 354 | "modules": "83", 355 | "lts": "Fermium", 356 | "security": false 357 | } 358 | "# 359 | .trim(); 360 | 361 | let result: OnlineNodeVersion = serde_json::from_str(json_str) 362 | .expect("Failed to parse version data from nodejs.org"); 363 | 364 | assert_that!(expected).is_equal_to(result); 365 | 366 | Ok(()) 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/subcommand/install.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, time::Duration}; 2 | 3 | use anyhow::{Context, Result}; 4 | use clap::Parser; 5 | use node_semver::Range; 6 | use ureq; 7 | 8 | use crate::{ 9 | archives, constants, files, 10 | node_version::{ 11 | filter_version_req, parse_range, InstalledNodeVersion, NodeVersion, OnlineNodeVersion, 12 | }, 13 | subcommand::{switch::SwitchCommand, Action}, 14 | Config, 15 | }; 16 | 17 | #[derive(Parser, Clone, Debug)] 18 | #[command(about = "Install a new node version", alias = "i", alias = "add")] 19 | pub struct InstallCommand { 20 | /// A semver range. The latest version matching this range will be installed 21 | #[arg(value_parser = parse_range)] 22 | pub version: Option, 23 | /// Switch to the new version after installing it 24 | #[arg(long, short, default_value("false"))] 25 | pub switch: bool, 26 | /// Enable corepack after installing the new version 27 | #[arg(long, default_value("true"), hide(true), env("NVM_ENABLE_COREPACK"))] 28 | pub enable_corepack: bool, 29 | } 30 | 31 | impl Action for InstallCommand { 32 | fn run(config: &Config, options: &InstallCommand) -> Result<()> { 33 | let version_filter = options 34 | .version 35 | .clone() 36 | .or_else(|| files::get_version_file().map(|version_file| version_file.range())); 37 | 38 | if version_filter.is_none() { 39 | anyhow::bail!("You did not pass a version and we did not find any version files (package.json#engines, .nvmrc) in the current directory."); 40 | } 41 | let version_filter = version_filter.unwrap(); 42 | 43 | let online_versions = OnlineNodeVersion::fetch_all()?; 44 | let filtered_versions = filter_version_req(online_versions, &version_filter); 45 | 46 | let version_to_install = filtered_versions.first().context(format!( 47 | "Did not find a version matching `{}`!", 48 | &version_filter 49 | ))?; 50 | 51 | if !config.force && InstalledNodeVersion::is_installed(config, version_to_install.version()) 52 | { 53 | println!( 54 | "{} is already installed - skipping...", 55 | version_to_install.version() 56 | ); 57 | 58 | return Ok(()); 59 | } 60 | 61 | let install_path = version_to_install.install_path(config); 62 | download_and_extract_to(version_to_install, &install_path)?; 63 | 64 | if config.force 65 | || (options.switch 66 | && dialoguer::Confirm::new() 67 | .with_prompt(format!("Switch to {}?", version_to_install.to_string())) 68 | .default(true) 69 | .interact()?) 70 | { 71 | SwitchCommand::run( 72 | &config.with_force(), 73 | &SwitchCommand { 74 | version: Some(Range::parse(version_to_install.to_string())?), 75 | }, 76 | )?; 77 | } 78 | 79 | if options.enable_corepack { 80 | if let Err(e) = std::process::Command::new( 81 | install_path 82 | .join("bin") 83 | .join(format!("corepack{}", constants::EXEC_EXT)), 84 | ) 85 | .arg("enable") 86 | .output() 87 | { 88 | println!("⚠️ Failed to automatically enable corepack!\n{e}",) 89 | } 90 | } 91 | 92 | Ok(()) 93 | } 94 | } 95 | 96 | fn download_and_extract_to(version: &OnlineNodeVersion, path: &Path) -> Result<()> { 97 | let url = version.download_url(); 98 | let agent = ureq::AgentBuilder::new() 99 | .timeout_connect(Duration::from_secs(30)) 100 | .timeout_read(Duration::from_secs(120)) 101 | .timeout_write(Duration::from_secs(120)) 102 | .build(); 103 | 104 | println!("Downloading from {url}..."); 105 | let response = agent 106 | .get(&url) 107 | .call() 108 | .context(format!("Failed to download version: {}", version.version()))?; 109 | 110 | let length: usize = response.header("Content-Length").unwrap().parse()?; 111 | let mut bytes: Vec = Vec::with_capacity(length); 112 | response.into_reader().read_to_end(&mut bytes)?; 113 | 114 | archives::extract_archive(bytes, path) 115 | } 116 | -------------------------------------------------------------------------------- /src/subcommand/is_installed.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use node_semver::Range; 4 | 5 | use crate::{ 6 | files, 7 | node_version::{parse_range, InstalledNodeVersion, NodeVersion}, 8 | subcommand::Action, 9 | Config, 10 | }; 11 | 12 | #[derive(Parser, Clone, Debug)] 13 | #[command( 14 | about = "Check if a version is installed", 15 | alias = "isi", 16 | alias = "installed" 17 | )] 18 | pub struct IsInstalledCommand { 19 | /// A semver range. Will be matched against installed all installed versions. 20 | #[arg(value_parser = parse_range)] 21 | pub version: Option, 22 | /// Which exit code to use when a version is not installed. 23 | #[arg(long, short = 'e', default_value = "1")] 24 | pub exit_code: i32, 25 | /// Silence output. 26 | #[arg(long, short = 'q')] 27 | pub quiet: bool, 28 | } 29 | 30 | impl Action for IsInstalledCommand { 31 | fn run(config: &Config, options: &IsInstalledCommand) -> Result<()> { 32 | let version_filter = options 33 | .version 34 | .clone() 35 | .or_else(|| files::get_version_file().map(|version_file| version_file.range())); 36 | 37 | if version_filter.is_none() { 38 | anyhow::bail!("You did not pass a version and we did not find any version files (package.json#engines, .nvmrc) in the current directory."); 39 | } 40 | let version_filter = version_filter.unwrap(); 41 | 42 | let installed_versions = InstalledNodeVersion::list(config); 43 | for installed_version in installed_versions { 44 | if !version_filter.satisfies(installed_version.version()) { 45 | continue; 46 | } 47 | 48 | if !options.quiet { 49 | println!( 50 | "✅ A version matching {version_filter} is installed ({})!", 51 | installed_version.to_string() 52 | ); 53 | } 54 | return Ok(()); 55 | } 56 | 57 | if !options.quiet { 58 | println!("❌ A version matching {version_filter} is not installed."); 59 | } 60 | 61 | std::process::exit(options.exit_code) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/subcommand/list.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use itertools::Itertools; 4 | use node_semver::Range; 5 | 6 | use crate::{ 7 | node_version, 8 | node_version::{parse_range, InstalledNodeVersion, NodeVersion, OnlineNodeVersion}, 9 | subcommand::Action, 10 | Config, 11 | }; 12 | 13 | enum VersionStatus<'p> { 14 | Latest, 15 | NotInstalled, 16 | Outdated(&'p OnlineNodeVersion), 17 | } 18 | 19 | impl<'p> VersionStatus<'p> { 20 | fn from(versions: &[&T], latest: &'p OnlineNodeVersion) -> VersionStatus<'p> { 21 | if versions.is_empty() { 22 | VersionStatus::NotInstalled 23 | } else if versions 24 | .iter() 25 | .all(|version| version.version() < latest.version()) 26 | { 27 | VersionStatus::Outdated(latest) 28 | } else { 29 | VersionStatus::Latest 30 | } 31 | } 32 | 33 | fn to_emoji(&self) -> char { 34 | match self { 35 | VersionStatus::Latest => '✅', 36 | VersionStatus::NotInstalled => '〰', 37 | VersionStatus::Outdated(_) => '⏫', 38 | } 39 | } 40 | 41 | fn to_version_string(&self) -> String { 42 | match self { 43 | VersionStatus::Outdated(version) => format!("-> {}", version.to_string()), 44 | _ => "".to_string(), 45 | } 46 | } 47 | } 48 | 49 | #[derive(Parser, Clone, Debug)] 50 | #[command(about = "List installed and released node versions", alias = "ls")] 51 | pub struct ListCommand { 52 | /// Only display installed versions 53 | #[arg(short, long, alias = "installed")] 54 | pub local: bool, 55 | /// Filter by semantic versions. 56 | /// 57 | /// `12`, `^10.9`, `>=8.10`, `>=8, <9` 58 | #[arg(short('F'), long, value_parser = parse_range)] 59 | pub filter: Option, 60 | } 61 | 62 | impl Action for ListCommand { 63 | fn run(config: &Config, options: &ListCommand) -> Result<()> { 64 | let mut installed_versions = InstalledNodeVersion::list(config); 65 | 66 | // Use filter option if it was passed 67 | if let Some(filter) = &options.filter { 68 | installed_versions = node_version::filter_version_req(installed_versions, filter); 69 | } 70 | 71 | if options.local { 72 | println!( 73 | "{}", 74 | installed_versions 75 | .iter() 76 | .map(|version| version.to_string()) 77 | .join("\n") 78 | ); 79 | 80 | return Ok(()); 81 | } 82 | 83 | // Get available versions, extract only the latest for each major version 84 | let mut latest_per_major = Vec::<&OnlineNodeVersion>::new(); 85 | let online_versions = OnlineNodeVersion::fetch_all()?; 86 | if !online_versions.is_empty() { 87 | latest_per_major = node_version::get_latest_of_each_major(&online_versions); 88 | latest_per_major.sort(); 89 | latest_per_major.reverse(); 90 | } 91 | 92 | let majors_and_installed_versions: Vec<(&OnlineNodeVersion, Vec<&InstalledNodeVersion>)> = 93 | latest_per_major 94 | .into_iter() 95 | .map(|latest| { 96 | ( 97 | latest, 98 | installed_versions 99 | .iter() 100 | .filter(|installed| installed.version().major == latest.version().major) 101 | .collect(), 102 | ) 103 | }) 104 | .collect(); 105 | 106 | // Show the latest X major versions by default 107 | // and show any older, installed versions as well 108 | let mut versions_to_show = Vec::<(&OnlineNodeVersion, &Vec<&InstalledNodeVersion>)>::new(); 109 | for (i, (latest, installed)) in majors_and_installed_versions.iter().enumerate() { 110 | if i < 5 || !installed.is_empty() { 111 | versions_to_show.push((latest, installed)); 112 | } 113 | } 114 | 115 | let output = versions_to_show 116 | .iter() 117 | .map(|(online_version, installed_versions)| { 118 | let version_status = VersionStatus::from(installed_versions, online_version); 119 | 120 | let version_to_show = if installed_versions.is_empty() { 121 | online_version.to_string() 122 | } else { 123 | installed_versions[0].to_string() 124 | }; 125 | 126 | format!( 127 | "{} {} {}", 128 | &version_status.to_emoji(), 129 | version_to_show, 130 | &version_status.to_version_string(), 131 | ) 132 | }) 133 | .join("\n"); 134 | 135 | println!("{output}"); 136 | Ok(()) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/subcommand/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::Config; 4 | 5 | pub mod install; 6 | pub mod is_installed; 7 | pub mod list; 8 | pub mod parse_version; 9 | pub mod switch; 10 | pub mod uninstall; 11 | 12 | pub trait Action { 13 | fn run(config: &Config, options: &T) -> Result<()>; 14 | } 15 | -------------------------------------------------------------------------------- /src/subcommand/parse_version.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use node_semver::Range; 4 | 5 | use crate::{files, node_version::parse_range, subcommand::Action, Config}; 6 | 7 | #[derive(Parser, Clone, Debug)] 8 | #[command( 9 | about = "Echo what a version string will be parsed to", 10 | alias = "pv", 11 | hide(true) 12 | )] 13 | pub struct ParseVersionCommand { 14 | /// The semver range to echo the parsed result of 15 | #[arg(value_parser = parse_range)] 16 | pub version: Option, 17 | } 18 | 19 | impl Action for ParseVersionCommand { 20 | fn run(_: &Config, options: &ParseVersionCommand) -> Result<()> { 21 | let version = options.version.clone(); 22 | 23 | if version.is_none() { 24 | if let Some(version_from_files) = files::get_version_file() { 25 | println!("{}", version_from_files.range()); 26 | 27 | return Ok(()); 28 | } 29 | } 30 | 31 | if version.is_none() { 32 | anyhow::bail!("Did not get a version"); 33 | } 34 | let version = version.unwrap(); 35 | 36 | match Range::parse(&version) { 37 | Ok(result) => { 38 | println!( 39 | "{:^pad$}\n{:^pad$}\n{}", 40 | version, 41 | "⬇", 42 | result, 43 | pad = result.to_string().len() 44 | ); 45 | Ok(()) 46 | }, 47 | Err(err) => { 48 | println!("Failed to parse `{}`", err.input()); 49 | Ok(()) 50 | }, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/subcommand/switch.rs: -------------------------------------------------------------------------------- 1 | use std::fs::read_link; 2 | #[cfg(windows)] 3 | use std::fs::remove_dir; 4 | #[cfg(unix)] 5 | use std::fs::remove_file; 6 | #[cfg(unix)] 7 | use std::os::unix::fs::symlink; 8 | #[cfg(windows)] 9 | use std::os::windows::fs::symlink_dir; 10 | 11 | use anyhow::Result; 12 | use clap::Parser; 13 | use node_semver::{Range, Version}; 14 | 15 | use crate::{ 16 | files, 17 | node_version::{parse_range, InstalledNodeVersion, NodeVersion}, 18 | subcommand::Action, 19 | Config, 20 | }; 21 | 22 | #[derive(Parser, Clone, Debug)] 23 | #[command(about = "Switch to an installed node version", alias = "switch")] 24 | pub struct SwitchCommand { 25 | /// A semver range. The latest version matching this range will be switched to. 26 | #[arg(value_parser = parse_range)] 27 | pub version: Option, 28 | } 29 | 30 | impl Action for SwitchCommand { 31 | fn run(config: &Config, options: &SwitchCommand) -> Result<()> { 32 | let version_filter = options 33 | .clone() 34 | .version 35 | .or_else(|| files::get_version_file().map(|version_file| version_file.range())); 36 | 37 | if version_filter.is_none() { 38 | anyhow::bail!("You did not pass a version and we did not find any version files (package.json#engines, .nvmrc) in the current directory."); 39 | } 40 | let version_filter = version_filter.unwrap(); 41 | 42 | let version = InstalledNodeVersion::find_matching(config, &version_filter); 43 | if version.is_none() { 44 | anyhow::bail!("No version matching the version range was found.") 45 | } 46 | 47 | let version = version.unwrap(); 48 | 49 | if !InstalledNodeVersion::is_installed(config, version.version()) { 50 | anyhow::bail!("{} is not installed", version.to_string()); 51 | } 52 | 53 | let result = set_shims(config, version.version()); 54 | if let Ok(()) = result { 55 | println!("Switched to {}", version.to_string()); 56 | } 57 | 58 | result 59 | } 60 | } 61 | 62 | #[cfg(windows)] 63 | fn set_shims(config: &Config, version: &Version) -> Result<()> { 64 | let shims_dir = config.get_shims_dir(); 65 | 66 | if !InstalledNodeVersion::is_installed(config, version) { 67 | anyhow::bail!("{version} is not installed"); 68 | } 69 | 70 | if read_link(&shims_dir).is_ok() { 71 | if let Err(err) = remove_dir(&shims_dir) { 72 | anyhow::bail!("Could not remove old symlink at {shims_dir:?}: {err}",); 73 | } 74 | } 75 | 76 | symlink_dir( 77 | config.get_versions_dir().join(version.to_string()), 78 | shims_dir, 79 | ) 80 | .map_err(anyhow::Error::from) 81 | } 82 | 83 | #[cfg(unix)] 84 | fn set_shims(config: &Config, version: &Version) -> Result<()> { 85 | let shims_dir = config.get_shims_dir(); 86 | 87 | if read_link(&shims_dir).is_ok() { 88 | remove_file(&shims_dir)?; 89 | } 90 | 91 | symlink( 92 | config 93 | .get_versions_dir() 94 | .join(version.to_string()) 95 | .join("bin"), 96 | shims_dir, 97 | ) 98 | .map_err(anyhow::Error::from) 99 | } 100 | -------------------------------------------------------------------------------- /src/subcommand/uninstall.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use node_semver::Range; 4 | 5 | use crate::{ 6 | node_version::{parse_range, InstalledNodeVersion, NodeVersion}, 7 | subcommand::Action, 8 | Config, 9 | }; 10 | 11 | #[derive(Parser, Clone, Debug)] 12 | #[command( 13 | about = "Uninstall a version", 14 | alias = "r", 15 | alias = "rm", 16 | alias = "remove" 17 | )] 18 | pub struct UninstallCommand { 19 | /// A semver range. The latest version matching this range will be installed 20 | #[arg(value_parser = parse_range)] 21 | pub version: Range, 22 | } 23 | 24 | impl Action for UninstallCommand { 25 | fn run(config: &Config, options: &UninstallCommand) -> Result<()> { 26 | let version = InstalledNodeVersion::find_matching(config, &options.version); 27 | if version.is_none() { 28 | anyhow::bail!("{} is not installed.", &options.version.to_string()) 29 | } 30 | 31 | let version = version.unwrap(); 32 | if version.is_selected(config) { 33 | println!("{} is currently selected.", version.version()); 34 | 35 | if !config.force 36 | && !(dialoguer::Confirm::new() 37 | .with_prompt("Are you sure you want to uninstall it?") 38 | .interact()?) 39 | { 40 | return Ok(()); 41 | } 42 | 43 | InstalledNodeVersion::deselect(config)?; 44 | } 45 | 46 | version.uninstall(config) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test-data/node-versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "14.6.0", 4 | "date": "2020-07-15", 5 | "files": [] 6 | }, 7 | { 8 | "version": "14.5.0", 9 | "date": "2020-06-30", 10 | "files": [] 11 | }, 12 | { 13 | "version": "13.14.0", 14 | "date": "2020-04-28", 15 | "files": [] 16 | }, 17 | { 18 | "version": "13.13.0", 19 | "date": "2020-04-14", 20 | "files": [] 21 | }, 22 | { 23 | "version": "12.18.3", 24 | "date": "2020-07-22", 25 | "files": [] 26 | }, 27 | { 28 | "version": "12.18.2", 29 | "date": "2020-06-30", 30 | "files": [] 31 | }, 32 | { 33 | "version": "11.15.0", 34 | "date": "2019-04-30", 35 | "files": [] 36 | }, 37 | { 38 | "version": "11.14.0", 39 | "date": "2019-04-10", 40 | "files": [] 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /tests/install_test.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | mod install { 4 | use crate::utils; 5 | use anyhow::Result; 6 | 7 | #[test] 8 | fn can_install_version_matching_range() -> Result<()> { 9 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 10 | 11 | let version_range = ">=14, <14.21"; 12 | let result = cmd 13 | .arg("install") 14 | .arg("--force") 15 | .arg(version_range) 16 | .assert(); 17 | 18 | utils::assert_outputs_contain( 19 | &result, 20 | "Downloading from https://nodejs.org/dist/v14.20.1/node-v14.20.1-", 21 | "", 22 | )?; 23 | utils::assert_version_installed(&temp_dir, "14.20.1", true)?; 24 | 25 | temp_dir.close().map_err(anyhow::Error::from) 26 | } 27 | 28 | #[test] 29 | fn can_install_version_matching_exact_version() -> Result<()> { 30 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 31 | 32 | let version_str = "14.21.2"; 33 | let result = cmd.arg("install").arg("--force").arg(version_str).assert(); 34 | 35 | utils::assert_outputs_contain( 36 | &result, 37 | "Downloading from https://nodejs.org/dist/v14.21.2/node-v14.21.2-", 38 | "", 39 | )?; 40 | utils::assert_version_installed(&temp_dir, version_str, true)?; 41 | 42 | temp_dir.close().map_err(anyhow::Error::from) 43 | } 44 | 45 | #[test] 46 | fn stops_when_installing_installed_version() -> Result<()> { 47 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 48 | 49 | let version_str = "14.21.2"; 50 | utils::install_mock_version(&temp_dir, version_str)?; 51 | 52 | let result = cmd.arg("install").arg(version_str).assert(); 53 | 54 | utils::assert_outputs_contain(&result, "14.21.2 is already installed - skipping...", "")?; 55 | 56 | temp_dir.close().map_err(anyhow::Error::from) 57 | } 58 | 59 | #[test] 60 | fn force_forces_install_of_installed_version() -> Result<()> { 61 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 62 | 63 | let version_str = "14.21.2"; 64 | let result = cmd.arg("install").arg("--force").arg(version_str).assert(); 65 | 66 | utils::assert_outputs_contain( 67 | &result, 68 | "Downloading from https://nodejs.org/dist/v14.21.2/node-v14.21.2-", 69 | "", 70 | )?; 71 | utils::assert_outputs_contain(&result, "Extracting...", "")?; 72 | utils::assert_version_installed(&temp_dir, version_str, true)?; 73 | 74 | temp_dir.close().map_err(anyhow::Error::from) 75 | } 76 | 77 | #[test] 78 | fn exits_gracefully_if_no_version_is_found() -> Result<()> { 79 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 80 | 81 | let result = cmd.arg("install").arg("--force").arg("12.99.99").assert(); 82 | 83 | utils::assert_outputs_contain( 84 | &result, 85 | "", 86 | "Error: Did not find a version matching `12.99.99`!", 87 | )?; 88 | 89 | temp_dir.close().map_err(anyhow::Error::from) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/switch_test.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | mod switch { 4 | use anyhow::Result; 5 | 6 | use crate::utils; 7 | 8 | #[test] 9 | fn can_switch_version_with_no_previous_one() -> Result<()> { 10 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 11 | 12 | let version_str = "12.18.3"; 13 | utils::install_mock_version(&temp_dir, version_str)?; 14 | let result = cmd.arg("use").arg("12").assert(); 15 | 16 | let output = String::from_utf8(result.get_output().to_owned().stdout)?; 17 | let output = output.trim(); 18 | 19 | assert_eq!(output, "Switched to 12.18.3"); 20 | assert_eq!( 21 | utils::get_selected_version(&temp_dir), 22 | Some(version_str.to_string()) 23 | ); 24 | 25 | temp_dir.close().map_err(anyhow::Error::from) 26 | } 27 | 28 | #[test] 29 | fn can_switch_version_with_previous_version() -> Result<()> { 30 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 31 | let old_version = "12.18.3"; 32 | let new_version = "14.5.0"; 33 | 34 | utils::install_mock_version(&temp_dir, old_version)?; 35 | utils::install_mock_version(&temp_dir, new_version)?; 36 | utils::create_shim(&temp_dir, old_version)?; 37 | 38 | let result = cmd.arg("use").arg("14").assert(); 39 | 40 | assert_eq!( 41 | utils::get_selected_version(&temp_dir), 42 | Some(new_version.to_string()) 43 | ); 44 | utils::assert_outputs_contain(&result, "Switched to 14.5.0", "")?; 45 | 46 | temp_dir.close().map_err(anyhow::Error::from) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/uninstall_test.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | mod uninstall { 4 | use anyhow::Result; 5 | use std::path::Path; 6 | 7 | use crate::utils; 8 | 9 | fn setup_versions(temp_dir: &Path, versions: Vec<&str>) -> Result<()> { 10 | for version_str in versions.iter().copied() { 11 | utils::install_mock_version(temp_dir, version_str)?; 12 | } 13 | 14 | utils::create_shim(temp_dir, versions.first().unwrap()) 15 | } 16 | 17 | #[test] 18 | fn can_uninstall_version_matching_range() -> Result<()> { 19 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 20 | 21 | let version_str = "12.18.3"; 22 | setup_versions(&temp_dir, vec!["14.5.0", version_str])?; 23 | 24 | let result = cmd.arg("uninstall").arg("12").assert(); 25 | 26 | utils::assert_outputs_contain(&result, "Uninstalled 12.18.3!", "")?; 27 | utils::assert_version_installed(&temp_dir, version_str, false)?; 28 | assert_eq!( 29 | utils::get_selected_version(&temp_dir), 30 | Some("14.5.0".to_string()) 31 | ); 32 | 33 | temp_dir.close().map_err(anyhow::Error::from) 34 | } 35 | 36 | #[test] 37 | fn can_uninstall_version_matching_exact_version() -> Result<()> { 38 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 39 | 40 | let version_str = "12.18.3"; 41 | setup_versions(&temp_dir, vec!["14.5.0", version_str])?; 42 | 43 | let result = cmd.arg("uninstall").arg(version_str).assert(); 44 | 45 | utils::assert_outputs_contain(&result, "Uninstalled 12.18.3!", "")?; 46 | utils::assert_version_installed(&temp_dir, version_str, false)?; 47 | assert_eq!( 48 | utils::get_selected_version(&temp_dir), 49 | Some("14.5.0".to_string()) 50 | ); 51 | 52 | temp_dir.close().map_err(anyhow::Error::from) 53 | } 54 | 55 | // #[test] 56 | // #[serial] 57 | // fn prompts_when_uninstalling_selected_version() -> Result<()> { 58 | // let version_str = "12.18.3"; 59 | // setup_versions(vec![version_str])?; 60 | // 61 | // let mut cmd = Command::cargo_bin("nvm-rust").unwrap(); 62 | // 63 | // let result = cmd.arg("uninstall").arg(version_str).assert(); 64 | // assert_outputs_contain( 65 | // &result, 66 | // "12.18.3 is currently selected.\nAre you sure you want to uninstall it? (y/N)", 67 | // "", 68 | // )?; 69 | // 70 | // cmd.write_stdin("y\n"); 71 | // 72 | // let result = cmd.assert(); 73 | // assert_outputs_contain( 74 | // &result, 75 | // "12.18.3 is currently selected.\nAre you sure you want to uninstall it? (y/N)\nUninstalled 12.18.3!", 76 | // "", 77 | // )?; 78 | // 79 | // assert_version_installed(version_str, false)?; 80 | // assert_version_selected(version_str, false)?; 81 | // 82 | // temp_dir.close().map_err(anyhow::Error::from) 83 | // } 84 | 85 | #[test] 86 | fn force_skips_prompt() -> Result<()> { 87 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 88 | 89 | let version_str = "12.18.3"; 90 | setup_versions(&temp_dir, vec![version_str])?; 91 | 92 | let result = cmd 93 | .arg("uninstall") 94 | .arg(version_str) 95 | .arg("--force") 96 | .assert(); 97 | 98 | utils::assert_outputs_contain( 99 | &result, 100 | "12.18.3 is currently selected.\nUninstalled 12.18.3!", 101 | "", 102 | )?; 103 | 104 | utils::assert_version_installed(&temp_dir, version_str, false)?; 105 | assert_eq!(utils::get_selected_version(&temp_dir), None); 106 | 107 | temp_dir.close().map_err(anyhow::Error::from) 108 | } 109 | 110 | #[test] 111 | fn exits_gracefully_if_no_version_is_found() -> Result<()> { 112 | let (temp_dir, mut cmd) = utils::setup_integration_test()?; 113 | 114 | setup_versions(&temp_dir, vec!["14.5.0"])?; 115 | 116 | let result = cmd.arg("uninstall").arg("12").assert(); 117 | 118 | utils::assert_outputs_contain(&result, "", "Error: >=12.0.0 <13.0.0-0 is not installed.")?; 119 | 120 | temp_dir.close().map_err(anyhow::Error::from) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/utils.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use std::os::unix::fs::symlink; 3 | #[cfg(windows)] 4 | use std::os::windows::fs::symlink_dir; 5 | use std::{fs, path::Path}; 6 | 7 | use anyhow::Result; 8 | use assert_cmd::{assert::Assert, Command}; 9 | use assert_fs::{prelude::*, TempDir}; 10 | use predicates::prelude::*; 11 | 12 | #[cfg(unix)] 13 | pub fn required_files<'a>() -> [&'a str; 3] { 14 | ["node", "npm", "npx"] 15 | } 16 | 17 | #[cfg(windows)] 18 | pub fn required_files<'a>() -> [&'a str; 5] { 19 | ["node.exe", "npm", "npm.cmd", "npx", "npx.cmd"] 20 | } 21 | 22 | fn integration_dir() -> TempDir { 23 | let dir = TempDir::new().expect("Could not create temp dir"); 24 | 25 | println!("{:#?}", dir.path()); 26 | dir 27 | } 28 | 29 | pub fn setup_integration_test() -> Result<(TempDir, Command)> { 30 | let temp_dir = integration_dir(); 31 | 32 | let mut cmd = Command::cargo_bin("nvm").expect("Could not create Command"); 33 | cmd.args(["--install-dir", &temp_dir.to_string_lossy()]); 34 | 35 | Ok((temp_dir, cmd)) 36 | } 37 | 38 | pub fn install_mock_version(path: &Path, version_str: &str) -> Result<()> { 39 | let mut to_dir = path.join("versions"); 40 | 41 | to_dir = to_dir.join(version_str); 42 | // Unix shims are under `bin/xxx` 43 | #[cfg(unix)] 44 | { 45 | to_dir = to_dir.join("bin"); 46 | } 47 | 48 | fs::create_dir_all(&to_dir)?; 49 | 50 | for file_name in required_files() { 51 | let file_path = to_dir.join(file_name); 52 | 53 | fs::write(&file_path, version_str) 54 | .unwrap_or_else(|err| panic!("Failed to write to {:#?}: {}", &file_path, err)) 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | #[allow(dead_code)] 61 | #[cfg(windows)] 62 | pub fn create_shim(temp_dir: &Path, version_str: &str) -> Result<()> { 63 | symlink_dir( 64 | temp_dir.join("versions").join(version_str), 65 | temp_dir.join("shims"), 66 | ) 67 | .map_err(anyhow::Error::from) 68 | } 69 | 70 | #[cfg(unix)] 71 | pub fn create_shim(temp_dir: &Path, version_str: &str) -> Result<()> { 72 | let mut shims_path = temp_dir.join("versions").join(version_str); 73 | 74 | // Unix shims are under `bin/xxx` 75 | #[cfg(unix)] 76 | { 77 | shims_path = shims_path.join("bin"); 78 | } 79 | 80 | symlink(&shims_path, temp_dir.join("shims")).map_err(anyhow::Error::from) 81 | } 82 | 83 | #[derive(PartialEq, Eq)] 84 | struct OutputResult(bool, bool); 85 | 86 | pub fn assert_outputs_contain(result: &Assert, stdout: &str, stderr: &str) -> Result<()> { 87 | let output = result.get_output().to_owned(); 88 | let output_stderr = String::from_utf8(output.stderr)?; 89 | let output_stdout = String::from_utf8(output.stdout)?; 90 | let result = OutputResult( 91 | output_stdout.trim().contains(stdout), 92 | output_stderr.trim().contains(stderr), 93 | ); 94 | 95 | if result != OutputResult(true, true) { 96 | panic!( 97 | r#"Got incorrect command output: 98 | stdout expected: 99 | "{}" 100 | stdout output: 101 | "{}" 102 | 103 | stderr expected: 104 | "{}" 105 | stderr output: 106 | "{}" 107 | "#, 108 | stdout, 109 | output_stdout.trim(), 110 | stderr, 111 | output_stderr.trim() 112 | ) 113 | } 114 | 115 | Ok(()) 116 | } 117 | 118 | #[allow(dead_code)] 119 | pub fn assert_version_installed( 120 | temp_dir: &TempDir, 121 | version_str: &str, 122 | expect_installed: bool, 123 | ) -> Result<()> { 124 | let versions_dir = temp_dir.child("versions"); 125 | 126 | for filename in required_files().iter() { 127 | let mut file_path = versions_dir.child(version_str); 128 | 129 | // Unix shims are under `bin/xxx` 130 | #[cfg(unix)] 131 | { 132 | file_path = file_path.child("bin"); 133 | } 134 | 135 | file_path = file_path.child(filename); 136 | 137 | if expect_installed { 138 | file_path.assert(predicates::path::exists()); 139 | } else { 140 | file_path.assert(predicates::path::exists().not()); 141 | } 142 | } 143 | 144 | Ok(()) 145 | } 146 | 147 | #[allow(dead_code)] 148 | pub fn get_selected_version(temp_dir: &TempDir) -> Option { 149 | let symlink_path = temp_dir.child("shims"); 150 | 151 | match fs::read_link(symlink_path) { 152 | Ok(shims_dir) => { 153 | let file_path = shims_dir.join(required_files()[0]); 154 | 155 | Some(fs::read_to_string(file_path).unwrap()) 156 | }, 157 | Err(_) => None, 158 | } 159 | } 160 | --------------------------------------------------------------------------------