Browse Source

Merge remote-tracking branch 'origin/master' into add-with-shims

remotes/origin/add-with-shims
Gal Schlezinger 2 years ago
parent
commit
db7a38c8de
  1. 8
      .changeset/README.md
  2. 5
      .changeset/chilly-apes-travel.md
  3. 14
      .changeset/config.json
  4. 5
      .changeset/red-flowers-collect.md
  5. 5
      .changeset/soft-laws-doubt.md
  6. 13
      .ci/generate-changelog.sh
  7. 94
      .ci/prepare-version.js
  8. 93
      .ci/print-command-docs.js
  9. 14
      .ci/record_screen.sh
  10. 11
      .ci/recorded_screen_script.sh
  11. 13
      .ci/type-letters.js
  12. 62
      .github/workflows/debug.yml
  13. 6
      .github/workflows/installation_script.yml
  14. 61
      .github/workflows/release.yml
  15. 210
      .github/workflows/rust.yml
  16. 2
      .gitignore
  17. 1
      .kodiak.toml
  18. 2
      .node-version
  19. 102
      CHANGELOG.md
  20. 1393
      Cargo.lock
  21. 45
      Cargo.toml
  22. 32
      README.md
  23. 802
      docs/commands.md
  24. 2
      docs/fnm.svg
  25. 269
      e2e/__snapshots__/aliases.test.ts.snap
  26. 284
      e2e/__snapshots__/basic.test.ts.snap
  27. 96
      e2e/__snapshots__/current.test.ts.snap
  28. 18
      e2e/__snapshots__/env.test.ts.snap
  29. 70
      e2e/__snapshots__/exec.test.ts.snap
  30. 28
      e2e/__snapshots__/existing-installation.test.ts.snap
  31. 32
      e2e/__snapshots__/latest-lts.test.ts.snap
  32. 127
      e2e/__snapshots__/log-level.test.ts.snap
  33. 67
      e2e/__snapshots__/multishell.test.ts.snap
  34. 32
      e2e/__snapshots__/nvmrc-lts.test.ts.snap
  35. 59
      e2e/__snapshots__/shims.test.ts.snap
  36. 46
      e2e/__snapshots__/uninstall.test.ts.snap
  37. 129
      e2e/aliases.test.ts
  38. 93
      e2e/basic.test.ts
  39. 47
      e2e/current.test.ts
  40. 15
      e2e/describe.ts
  41. 35
      e2e/env.test.ts
  42. 42
      e2e/exec.test.ts
  43. 22
      e2e/existing-installation.test.ts
  44. 19
      e2e/latest-lts.test.ts
  45. 71
      e2e/log-level.test.ts
  46. 27
      e2e/multishell.test.ts
  47. 24
      e2e/nvmrc-lts.test.ts
  48. 3
      e2e/shellcode/get-stderr.ts
  49. 179
      e2e/shellcode/script.ts
  50. 97
      e2e/shellcode/shells.ts
  51. 12
      e2e/shellcode/shells/cmdCall.ts
  52. 33
      e2e/shellcode/shells/cmdEnv.ts
  53. 52
      e2e/shellcode/shells/expect-command-output.ts
  54. 24
      e2e/shellcode/shells/output-contains.ts
  55. 16
      e2e/shellcode/shells/redirect-output.ts
  56. 20
      e2e/shellcode/shells/sub-shell.ts
  57. 15
      e2e/shellcode/shells/types.ts
  58. 9
      e2e/shellcode/test-bin-dir.ts
  59. 9
      e2e/shellcode/test-cwd.ts
  60. 10
      e2e/shellcode/test-node-version.ts
  61. 15
      e2e/shellcode/test-tmp-dir.ts
  62. 49
      e2e/shims.test.ts
  63. 38
      e2e/system-node.test.ts
  64. 29
      e2e/uninstall.test.ts
  65. 22
      jest.config.cjs
  66. 33
      package.json
  67. 5129
      pnpm-lock.yaml
  68. 28
      renovate.json
  69. 5
      site/package.json
  70. 10
      site/tsconfig.json
  71. 14
      site/vercel.json
  72. 677
      site/yarn.lock
  73. 26
      src/arch.rs
  74. 4
      src/archive/zip.rs
  75. 24
      src/choose_version_for_user_input.rs
  76. 38
      src/cli.rs
  77. 22
      src/commands/alias.rs
  78. 21
      src/commands/completions.rs
  79. 3
      src/commands/current.rs
  80. 3
      src/commands/default.rs
  81. 124
      src/commands/env.rs
  82. 72
      src/commands/exec.rs
  83. 70
      src/commands/install.rs
  84. 20
      src/commands/ls_local.rs
  85. 15
      src/commands/ls_remote.rs
  86. 16
      src/commands/unalias.rs
  87. 70
      src/commands/uninstall.rs
  88. 138
      src/commands/use.rs
  89. 83
      src/config.rs
  90. 18
      src/current_version.rs
  91. 9
      src/default_version.rs
  92. 26
      src/directories.rs
  93. 65
      src/downloader.rs
  94. 28
      src/installed_versions.rs
  95. 14
      src/log_level.rs
  96. 4
      src/main.rs
  97. 43
      src/shell/bash.rs
  98. 39
      src/shell/fish.rs
  99. 45
      src/shell/infer/mod.rs
  100. 121
      src/shell/infer/unix.rs
  101. Some files were not shown because too many files have changed in this diff Show More

8
.changeset/README.md

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

5
.changeset/chilly-apes-travel.md

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
---
"fnm": minor
---
Add `--json` to `fnm env` to output the env vars as JSON

14
.changeset/config.json

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
"changelog": [
"@svitejs/changesets-changelog-github-compact",
{ "repo": "Schniz/fnm" }
],
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
}

5
.changeset/red-flowers-collect.md

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
---
"fnm": patch
---
This updates the Changesets configurations.

5
.changeset/soft-laws-doubt.md

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
---
"fnm": patch
---
fix test: Use correct PATH for npm install test

13
.ci/generate-changelog.sh

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
#!/bin/bash
if [ "$1" = "" ]; then
echo "No version provided, using 'Unreleased'" >&2
NEXT_VERSION="Unreleased"
else
NEXT_VERSION="$1"
fi
echo "Generating changelog for $NEXT_VERSION"
lerna-changelog --from=v1.0.0 "--next-version=$NEXT_VERSION" >CHANGELOG.md
prettier --write CHANGELOG.md

94
.ci/prepare-version.js

@ -2,85 +2,71 @@ @@ -2,85 +2,71 @@
/// @ts-check
const fs = require("fs");
const cp = require("child_process");
const path = require("path");
const cmd = require("cmd-ts");
const toml = require("toml");
import fs from "fs"
import cp from "child_process"
import cmd from "cmd-ts"
import toml from "toml"
import assert from "assert"
const CARGO_TOML_PATH = path.join(__dirname, "../Cargo.toml");
const CARGO_TOML_PATH = new URL("../Cargo.toml", import.meta.url).pathname
const command = cmd.command({
name: "prepare-version",
description: "Prepare a new fnm version",
args: {
versionType: cmd.positional({
displayName: "version type",
type: cmd.oneOf(["patch", "minor", "major"]),
}),
args: {},
async handler({}) {
updateCargoToml(await getPackageVersion())
exec("cargo build --release")
exec("yarn generate-command-docs --binary-path=./target/release/fnm")
exec("./.ci/record_screen.sh")
},
async handler({ versionType }) {
exec("git pull --ff-only");
const nextVersion = updateCargoToml(versionType);
exec("cargo build --release");
exec("yarn generate-command-docs --binary-path=./target/release/fnm");
exec("./docs/record_screen.sh");
exec(`yarn changelog ${nextVersion}`);
},
});
})
cmd.run(cmd.binary(command), process.argv);
cmd.run(cmd.binary(command), process.argv)
//////////////////////
// Helper functions //
//////////////////////
function updateCargoToml(versionType) {
const cargoToml = fs.readFileSync(CARGO_TOML_PATH, "utf8");
const cargoTomlContents = toml.parse(cargoToml);
const currentVersion = cargoTomlContents.package.version;
const nextVersion = changeVersion(
versionType,
cargoTomlContents.package.version
);
/**
* @returns {Promise<string>}
*/
async function getPackageVersion() {
const pkgJson = await fs.promises.readFile(
new URL("../package.json", import.meta.url),
"utf8"
)
const version = JSON.parse(pkgJson).version
assert(version, "package.json version is not set")
return version
}
function updateCargoToml(nextVersion) {
const cargoToml = fs.readFileSync(CARGO_TOML_PATH, "utf8")
const cargoTomlContents = toml.parse(cargoToml)
const currentVersion = cargoTomlContents.package.version
const newToml = cargoToml.replace(
`version = "${currentVersion}"`,
`version = "${nextVersion}"`
);
)
if (newToml === cargoToml) {
console.error("Cargo.toml didn't change, error!");
process.exitCode = 1;
return;
console.error("Cargo.toml didn't change, error!")
process.exitCode = 1
return
}
fs.writeFileSync(CARGO_TOML_PATH, newToml, "utf8");
fs.writeFileSync(CARGO_TOML_PATH, newToml, "utf8")
return nextVersion;
return nextVersion
}
function exec(command, env) {
console.log(`$ ${command}`);
console.log(`$ ${command}`)
return cp.execSync(command, {
cwd: path.join(__dirname, ".."), // root of repo
cwd: new URL("..", import.meta.url),
stdio: "inherit",
env: { ...process.env, ...env },
});
}
/**
* @param {"patch" | "minor" | "major"} type
* @param {string} version
*/
function changeVersion(type, version) {
const [major, minor, patch] = version.split(".").map((x) => parseInt(x, 10));
switch (type) {
case "patch":
return [major, minor, patch + 1].join(".");
case "minor":
return [major, minor + 1, 0].join(".");
case "major":
return [major + 1, 0, 0].join(".");
}
})
}

93
.ci/print-command-docs.js

@ -2,24 +2,23 @@ @@ -2,24 +2,23 @@
/// @ts-check
const execa = require("execa");
const path = require("path");
const fs = require("fs");
const cmd = require("cmd-ts");
const cmdFs = require("cmd-ts/dist/cjs/batteries/fs");
import { execa } from "execa"
import fs from "node:fs"
import cmd from "cmd-ts"
import cmdFs from "cmd-ts/dist/cjs/batteries/fs.js"
const FnmBinaryPath = {
...cmdFs.ExistingPath,
defaultValue() {
const target = path.join(__dirname, "../target/debug/fnm");
const target = new URL("../target/debug/fnm", import.meta.url)
if (!fs.existsSync(target)) {
throw new Error(
"Can't find debug target, please run `cargo build` or provide a specific binary path"
);
)
}
return target;
return target.pathname
},
};
}
const command = cmd.command({
name: "print-command-docs",
@ -36,27 +35,27 @@ const command = cmd.command({ @@ -36,27 +35,27 @@ const command = cmd.command({
}),
},
async handler({ checkForDirty, fnmPath }) {
const targetFile = path.join(__dirname, "../docs/commands.md");
await main(targetFile, fnmPath);
const targetFile = new URL("../docs/commands.md", import.meta.url).pathname
await main(targetFile, fnmPath)
if (checkForDirty) {
const gitStatus = await checkGitStatus(targetFile);
const gitStatus = await checkGitStatus(targetFile)
if (gitStatus.state === "dirty") {
process.exitCode = 1;
process.exitCode = 1
console.error(
"The file has changed. Please re-run `yarn generate-command-docs`."
);
console.error(`hint: The following diff was found:`);
console.error();
console.error(gitStatus.diff);
)
console.error(`hint: The following diff was found:`)
console.error()
console.error(gitStatus.diff)
}
}
},
});
})
cmd.run(cmd.binary(command), process.argv).catch((err) => {
console.error(err);
process.exitCode = process.exitCode || 1;
});
console.error(err)
process.exitCode = process.exitCode || 1
})
/**
* @param {string} targetFile
@ -64,20 +63,20 @@ cmd.run(cmd.binary(command), process.argv).catch((err) => { @@ -64,20 +63,20 @@ cmd.run(cmd.binary(command), process.argv).catch((err) => {
* @returns {Promise<void>}
*/
async function main(targetFile, fnmPath) {
const stream = fs.createWriteStream(targetFile);
const stream = fs.createWriteStream(targetFile)
const { subcommands, text: mainText } = await getCommandHelp(fnmPath);
const { subcommands, text: mainText } = await getCommandHelp(fnmPath)
await write(stream, line(`fnm`, mainText));
await write(stream, line(`fnm`, mainText))
for (const subcommand of subcommands) {
const { text: subcommandText } = await getCommandHelp(fnmPath, subcommand);
await write(stream, "\n" + line(`fnm ${subcommand}`, subcommandText));
const { text: subcommandText } = await getCommandHelp(fnmPath, subcommand)
await write(stream, "\n" + line(`fnm ${subcommand}`, subcommandText))
}
stream.close();
stream.close()
await execa(`yarn`, ["prettier", "--write", targetFile]);
await execa(`yarn`, ["prettier", "--write", targetFile])
}
/**
@ -87,14 +86,14 @@ async function main(targetFile, fnmPath) { @@ -87,14 +86,14 @@ async function main(targetFile, fnmPath) {
*/
function write(stream, content) {
return new Promise((resolve, reject) => {
stream.write(content, (err) => (err ? reject(err) : resolve()));
});
stream.write(content, (err) => (err ? reject(err) : resolve()))
})
}
function line(cmd, text) {
const cmdCode = "`" + cmd + "`";
const textCode = "```\n" + text + "\n```";
return `# ${cmdCode}\n${textCode}`;
const cmdCode = "`" + cmd + "`"
const textCode = "```\n" + text + "\n```"
return `# ${cmdCode}\n${textCode}`
}
/**
@ -103,23 +102,25 @@ function line(cmd, text) { @@ -103,23 +102,25 @@ function line(cmd, text) {
* @returns {Promise<{ subcommands: string[], text: string }>}
*/
async function getCommandHelp(fnmPath, command) {
const cmdArg = command ? [command] : [];
const result = await run(fnmPath, [...cmdArg, "--help"]);
const text = result.stdout;
const rows = text.split("\n");
const headerIndex = rows.findIndex((x) => x.includes("SUBCOMMANDS"));
const cmdArg = command ? [command] : []
const result = await run(fnmPath, [...cmdArg, "--help"])
const text = result.stdout
const rows = text.split("\n")
const headerIndex = rows.findIndex((x) => x.includes("SUBCOMMANDS"))
/** @type {string[]} */
const subcommands = [];
const subcommands = []
if (!command) {
for (const row of rows.slice(headerIndex + 1)) {
const words = row.split(/\s+/);
subcommands.push(words[1]);
const [, word] = row.split(/\s+/)
if (word && word[0].toLowerCase() === word[0]) {
subcommands.push(word)
}
}
}
return {
subcommands,
text,
};
}
}
/**
@ -131,7 +132,7 @@ function run(fnmPath, args) { @@ -131,7 +132,7 @@ function run(fnmPath, args) {
reject: false,
stdout: "pipe",
stderr: "pipe",
});
})
}
/**
@ -145,9 +146,9 @@ async function checkGitStatus(targetFile) { @@ -145,9 +146,9 @@ async function checkGitStatus(targetFile) {
{
reject: false,
}
);
)
if (exitCode === 0) {
return { state: "clean" };
return { state: "clean" }
}
return { state: "dirty", diff: stdout };
return { state: "dirty", diff: stdout }
}

14
docs/record_screen.sh → .ci/record_screen.sh

@ -3,10 +3,15 @@ @@ -3,10 +3,15 @@
DIRECTORY="$(dirname "$0")"
function setup_binary() {
TEMP_DIR="$(mktemp -d -t fnm)"
TEMP_DIR="/tmp/fnm-$(date '+%s')"
mkdir "$TEMP_DIR"
cp ./target/release/fnm "$TEMP_DIR/fnm"
export PATH=$TEMP_DIR:$PATH
export FNM_DIR=$TEMP_DIR/.fnm
# First run of the binary might be slower due to anti-virus software
echo "Using $(which fnm)"
echo " with version $(fnm --version)"
}
setup_binary
@ -16,4 +21,9 @@ RECORDING_PATH=$DIRECTORY/screen_recording @@ -16,4 +21,9 @@ RECORDING_PATH=$DIRECTORY/screen_recording
(rm -rf "$RECORDING_PATH" &> /dev/null || true)
asciinema rec -c "$DIRECTORY/recorded_screen_script.sh" "$RECORDING_PATH"
sed "s@$TEMP_DIR@~@g" "$RECORDING_PATH" | svg-term --window --out "$DIRECTORY/fnm.svg" --height=17 --width=70
sed "s@$TEMP_DIR@~@g" "$RECORDING_PATH" | \
svg-term \
--window \
--out "docs/fnm.svg" \
--height=17 \
--width=70

11
docs/recorded_screen_script.sh → .ci/recorded_screen_script.sh

@ -1,16 +1,19 @@ @@ -1,16 +1,19 @@
#!/bin/zsh
#!/bin/bash
set -e
GAL_PROMPT_PREFIX='\e[34m✡ \e[0m'
export PATH=$PATH_ADDITION:$PATH
GAL_PROMPT_PREFIX="\e[34m✡\e[m "
function type() {
printf $GAL_PROMPT_PREFIX
echo $* | pv -qL $[10+(-2 + RANDOM%5)]
echo -n " "
echo $* | node .ci/type-letters.js
}
type 'eval "$(fnm env)"'
eval `fnm env`
eval "$(fnm env)"
type 'fnm --version'
fnm --version

13
.ci/type-letters.js

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
(async () => {
for await (const chunk of process.stdin) {
const letters = chunk.toString("utf8").split("");
for (const letter of letters) {
process.stdout.write(letter);
await sleep(Math.random() * 100 + 20);
}
}
})();
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

62
.github/workflows/debug.yml

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
name: "debug"
on:
workflow_dispatch:
concurrency:
group: debug
cancel-in-progress: true
jobs:
e2e_windows_debug:
runs-on: windows-latest
name: "e2e/windows/debug"
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.commit_hash }}
- name: Download artifact
id: download-artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: rust.yml
workflow_conclusion: ""
branch: ${{ env.GITHUB_REF }}
name: "fnm-windows"
path: "target/release"
if_no_artifact_found: ignore
- uses: hecrj/setup-rust-action@v1
if: steps.download-artifact.outputs.artifact-found == false
with:
rust-version: stable
- uses: Swatinem/rust-cache@v2
if: steps.download-artifact.outputs.artifact-found == false
- name: Build release binary
if: steps.download-artifact.outputs.artifact-found == false
run: cargo build --release
env:
RUSTFLAGS: "-C target-feature=+crt-static"
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install
- name: 🐛 Debug Build
if: always()
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true

6
.github/workflows/installation_script.yml

@ -21,8 +21,8 @@ jobs: @@ -21,8 +21,8 @@ jobs:
steps:
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
- uses: actions/checkout@v2
uses: docker/setup-qemu-action@v2
- uses: actions/checkout@v3
- name: Run installation script in Docker
run: |
docker run --rm -v $(pwd):$(pwd) -e "RUST_LOG=fnm=debug" --workdir $(pwd) ${{matrix.docker_image}} bash -c '
@ -62,7 +62,7 @@ jobs: @@ -62,7 +62,7 @@ jobs:
script_arguments: '--force-no-brew'
runs-on: ${{ matrix.setup.os }}-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- run: "sudo apt-get install -y ${{ matrix.shell }}"
name: Install ${{matrix.shell}} using apt-get
if: matrix.setup.os == 'ubuntu'

61
.github/workflows/release.yml

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
name: release
on:
push:
branches:
- master
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
create_pull_request:
runs-on: ubuntu-latest
steps:
# set up
- uses: actions/checkout@v3
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: Swatinem/rust-cache@v2
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
# pnpm
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install script dependencies
run: |
sudo apt-get update
sudo apt-get install -y asciinema
- name: Install Node.js project dependencies
run: pnpm install
- name: Create Release Pull Request
uses: changesets/action@v1
with:
version: "pnpm version:prepare"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TERM: xterm

210
.github/workflows/rust.yml

@ -6,6 +6,10 @@ on: @@ -6,6 +6,10 @@ on:
branches:
- master
concurrency:
group: ci-${{ github.head_ref }}
cancel-in-progress: true
jobs:
fmt:
runs-on: ubuntu-latest
@ -13,7 +17,8 @@ jobs: @@ -13,7 +17,8 @@ jobs:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- name: cargo fmt
run: cargo fmt -- --check
@ -23,7 +28,8 @@ jobs: @@ -23,7 +28,8 @@ jobs:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- name: cargo clippy
run: cargo clippy -- -D warnings
@ -36,28 +42,10 @@ jobs: @@ -36,28 +42,10 @@ jobs:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: actions/checkout@v2
- name: Run tests
run: cargo test -- --skip=feature_tests
e2e_tests:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- name: Install Fish and Zsh using brew
if: "startsWith(matrix.os, 'macOS')"
run: brew install fish zsh
- name: Install Fish and Zsh using apt
if: "startsWith(matrix.os, 'ubuntu')"
run: sudo apt-get install -y fish zsh
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- name: Run tests
run: cargo test -- feature_tests
run: cargo test
build_release:
runs-on: windows-latest
@ -66,12 +54,13 @@ jobs: @@ -66,12 +54,13 @@ jobs:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- name: Build release binary
run: cargo build --release
env:
RUSTFLAGS: "-C target-feature=+crt-static"
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: fnm-windows
path: target/release/fnm.exe
@ -83,7 +72,8 @@ jobs: @@ -83,7 +72,8 @@ jobs:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- name: Build release binary
run: cargo build --release
env:
@ -92,11 +82,155 @@ jobs: @@ -92,11 +82,155 @@ jobs:
run: strip target/release/fnm
- name: List dynamically linked libraries
run: otool -L target/release/fnm
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: fnm-macos
path: target/release/fnm
e2e_macos:
runs-on: macos-latest
needs: [build_macos_release]
name: "e2e/macos"
steps:
- name: install necessary shells
run: brew install fish zsh bash
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: fnm-macos
path: target/release
- name: mark binary as executable
run: chmod +x target/release/fnm
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install
- run: pnpm test
env:
FNM_TARGET_NAME: "release"
FORCE_COLOR: "1"
e2e_windows:
runs-on: windows-latest
needs: [build_release]
name: "e2e/windows"
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: fnm-windows
path: target/release
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install
- run: pnpm test
env:
FNM_TARGET_NAME: "release"
FORCE_COLOR: "1"
# e2e_windows_debug:
# runs-on: windows-latest
# name: "e2e/windows/debug"
# environment: Debug
# needs: [e2e_windows]
# if: contains(join(needs.*.result, ','), 'failure')
# steps:
# - uses: actions/checkout@v3
# - uses: actions/download-artifact@v3
# with:
# name: fnm-windows
# path: target/release
# - uses: pnpm/action-setup@v2.2.2
# with:
# run_install: false
# - uses: actions/setup-node@v3
# with:
# node-version: 16.x
# cache: 'pnpm'
# - name: Get pnpm store directory
# id: pnpm-cache
# run: |
# echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
# - uses: actions/cache@v3
# name: Setup pnpm cache
# with:
# path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
# key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
# restore-keys: |
# ${{ runner.os }}-pnpm-store-
# - run: pnpm install
# - name: 🐛 Debug Build
# uses: mxschmitt/action-tmate@v3
e2e_linux:
runs-on: ubuntu-latest
needs: [build_static_linux_binary]
name: "e2e/linux"
steps:
- name: install necessary shells
run: sudo apt-get update && sudo apt-get install -y fish zsh bash
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: fnm-linux
path: target/release
- name: mark binary as executable
run: chmod +x target/release/fnm
- uses: pnpm/action-setup@v2.2.4
with:
run_install: false
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install
- run: pnpm test
env:
FNM_TARGET_NAME: "release"
FORCE_COLOR: "1"
build_static_linux_binary:
name: "Build static Linux binary"
runs-on: ubuntu-latest
@ -105,16 +239,19 @@ jobs: @@ -105,16 +239,19 @@ jobs:
with:
rust-version: stable
targets: x86_64-unknown-linux-musl
- uses: Swatinem/rust-cache@v2
with:
key: static-linux-binary
- name: Install musl tools
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends musl-tools
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Build release binary
run: cargo build --release --target x86_64-unknown-linux-musl
- name: Strip binary from debug symbols
run: strip target/x86_64-unknown-linux-musl/release/fnm
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: fnm-linux
path: target/x86_64-unknown-linux-musl/release/fnm
@ -138,20 +275,23 @@ jobs: @@ -138,20 +275,23 @@ jobs:
steps:
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
- uses: Swatinem/rust-cache@v2
with:
key: arm-binary-${{ matrix.arch }}
- name: 'Download `cross` crate'
run: cargo install cross
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: "Build release"
run: cross build --target $RUST_TARGET --release
- name: Compress binary using UPX
run: |
sudo apt-get install -y upx
upx target/$RUST_TARGET/release/fnm
- uses: uraimo/run-on-arch-action@v2.1.1
- uses: uraimo/run-on-arch-action@v2.1.2
name: Sanity test
with:
arch: ${{matrix.docker_platform}}
@ -176,7 +316,7 @@ jobs: @@ -176,7 +316,7 @@ jobs:
echo "fnm exec --using=12 -- node --version"
/artifacts/fnm exec --using=12 -- node --version
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: fnm-${{ matrix.arch }}
path: target/${{ env.RUST_TARGET }}/release/fnm
@ -186,9 +326,9 @@ jobs: @@ -186,9 +326,9 @@ jobs:
name: Ensure command docs are up-to-date
needs: [build_static_linux_binary]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Download a single artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: fnm-linux
- name: Make the binary runnable

2
.gitignore vendored

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
node_modules/
docs/screen_recording
.ci/screen_recording
/benchmarks/results
/target
**/*.rs.bk

1
.kodiak.toml

@ -0,0 +1 @@ @@ -0,0 +1 @@
version = 1

2
.node-version

@ -1 +1 @@ @@ -1 +1 @@
16.13.1
16.18.1

102
CHANGELOG.md

@ -1,4 +1,102 @@ @@ -1,4 +1,102 @@
## 1.28.2 (2021-12-05)
## 1.31.0 (2022-02-16)
## 1.31.1
### Patch Changes
- 6e6bdd8: Add changesets
#### Bugfix 🐛
- [#671](https://github.com/Schniz/fnm/pull/671) fix native-creds errors (using the unpublished version of reqwest) ([@Schniz](https://github.com/Schniz))
- [#663](https://github.com/Schniz/fnm/pull/663) remove .unwrap() and .expect() in shell code ([@Schniz](https://github.com/Schniz))
- [#656](https://github.com/Schniz/fnm/pull/656) Remove unwrap in fnm env, make error more readable ([@Schniz](https://github.com/Schniz))
#### Committers: 1
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
## v1.30.1 (2022-01-30)
#### Bugfix 🐛
- [#652](https://github.com/Schniz/fnm/pull/652) Fix `fnm completions` ([@Schniz](https://github.com/Schniz))
#### Committers: 1
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
## v1.30.0 (2022-01-30)
#### Bugfix 🐛
- [#625](https://github.com/Schniz/fnm/pull/625) Revert from using sysinfo, but only on Unix machines ([@Schniz](https://github.com/Schniz))
- [#638](https://github.com/Schniz/fnm/pull/638) Use local cache directory instead of temp directory for symlinks ([@Schniz](https://github.com/Schniz))
#### Internal 🛠
- [#617](https://github.com/Schniz/fnm/pull/617) fix(deps): update rust crate clap to v3 ([@renovate[bot]](https://github.com/apps/renovate))
- [#630](https://github.com/Schniz/fnm/pull/630) Replace `snafu` with `thiserror` ([@Schniz](https://github.com/Schniz))
#### Documentation 📝
- [#637](https://github.com/Schniz/fnm/pull/637) [Documentation] Adds Additional info regarding .node-version ([@uchihamalolan](https://github.com/uchihamalolan))
#### Committers: 2
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
- Malolan B ([@uchihamalolan](https://github.com/uchihamalolan))
## v1.29.2 (2022-01-06)
#### Bugfix 🐛
- [#626](https://github.com/Schniz/fnm/pull/626) Create the multishells directory if it is missing ([@Schniz](https://github.com/Schniz))
#### Documentation 📝
- [#598](https://github.com/Schniz/fnm/pull/598) Update README.md file emoji ([@lorensr](https://github.com/lorensr))
- [#616](https://github.com/Schniz/fnm/pull/616) Use on cd by default ([@matthieubosquet](https://github.com/matthieubosquet))
#### Committers: 3
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
- Loren ☺ ([@lorensr](https://github.com/lorensr))
- Matthieu Bosquet ([@matthieubosquet](https://github.com/matthieubosquet))
## v1.29.1 (2021-12-28)
#### Bugfix 🐛
- [#613](https://github.com/Schniz/fnm/pull/613) Don't warn on `~/.fnm` yet ([@Schniz](https://github.com/Schniz))
#### Committers: 1
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
## v1.29.0 (2021-12-28)
#### New Feature 🎉
- [#607](https://github.com/Schniz/fnm/pull/607) Allow recursive version lookups ([@Schniz](https://github.com/Schniz))
- [#416](https://github.com/Schniz/fnm/pull/416) Respect \$XDG_DATA_HOME ([@samhh](https://github.com/samhh))
#### Bugfix 🐛
- [#603](https://github.com/Schniz/fnm/pull/603) (re)Include feature for (native) certificates ([@pfiaux](https://github.com/pfiaux))
- [#605](https://github.com/Schniz/fnm/pull/605) Add a user-agent header ([@Schniz](https://github.com/Schniz))
#### Internal 🛠
- [#606](https://github.com/Schniz/fnm/pull/606) Migrate to Rust 2021 edition ([@Schniz](https://github.com/Schniz))
#### Committers: 3
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
- Patrick Fiaux ([@pfiaux](https://github.com/pfiaux))
- Sam A. Horvath-Hunt ([@samhh](https://github.com/samhh))
## v1.28.2 (2021-12-05)
#### Bugfix 🐛
@ -197,7 +295,7 @@ @@ -197,7 +295,7 @@
#### Bugfix 🐛
- [#368](https://github.com/Schniz/fnm/pull/368) Update 'ring' to '0.16.17' ([@gucheen](https://github.com/gucheen))
- [#347](https://github.com/Schniz/fnm/pull/347) Check if $ZDOTDIR exists to find correct path to .zshrc ([@thales-maciel](https://github.com/thales-maciel))
- [#347](https://github.com/Schniz/fnm/pull/347) Check if \$ZDOTDIR exists to find correct path to .zshrc ([@thales-maciel](https://github.com/thales-maciel))
#### Documentation 📝

1393
Cargo.lock generated

File diff suppressed because it is too large Load Diff

45
Cargo.toml

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
[package]
name = "fnm"
version = "1.28.2"
version = "1.31.1"
authors = ["Gal Schlezinger <gal@spitfire.co.il>"]
edition = "2021"
build = "build.rs"
@ -9,38 +9,39 @@ repository = "https://github.com/Schniz/fnm" @@ -9,38 +9,39 @@ repository = "https://github.com/Schniz/fnm"
description = "Fast and simple Node.js version manager"
[dependencies]
serde = { version = "1.0.132", features = ["derive"] }
clap = "2.34.0"
structopt = "0.3.25"
serde_json = "1.0.73"
chrono = { version = "0.4.19", features = ["serde"] }
serde = { version = "1.0.145", features = ["derive"] }
clap = { version = "3.2.23", features = ["derive", "env"] }
serde_json = "1.0.85"
chrono = { version = "0.4.23", features = ["serde"] }
tar = "0.4.38"
xz2 = "0.1.6"
semver = "1.0.4"
xz2 = "0.1.7"
semver = "1.0.14"
dirs = "4.0.0"
colored = "2.0.0"
zip = "0.5.13"
tempfile = "3.2.0"
indoc = "1.0.3"
snafu = { version = "0.6.10", features = ["backtrace"] }
log = "0.4.14"
env_logger = "0.9.0"
zip = "0.6.3"
tempfile = "3.3.0"
indoc = "1.0.7"
log = "0.4.17"
env_logger = "0.9.3"
atty = "0.2.14"
encoding_rs_io = "0.1.7"
reqwest = { version = "0.11.7", features = ["blocking", "json", "rustls-tls", "brotli"], default-features = false }
url = "2.2.2"
sysinfo = "0.22.3"
reqwest = { version = "0.11.12", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "brotli"], default-features = false }
url = "2.3.1"
sysinfo = "0.26.7"
thiserror = "1.0.37"
clap_complete = "3.2.5"
anyhow = "1.0.65"
[dev-dependencies]
pretty_assertions = "1.0.0"
pretty_assertions = "1.3.0"
duct = "0.13.5"
shell-escape = "0.1.5"
insta = { version = "1.8.0", features = ["backtrace"] }
serial_test = "0.5.1"
test-log = "0.2.8"
insta = "1.21.0"
serial_test = "0.9.0"
test-log = "0.2.11"
[build-dependencies]
embed-resource = "1.6.5"
embed-resource = "1.7.3"
[target.'cfg(windows)'.dependencies]
csv = "1.1.6"

32
README.md

@ -18,13 +18,15 @@ @@ -18,13 +18,15 @@
:rocket: Built with speed in mind
:thinking: Works with `.node-version` and `.nvmrc` files
:open_file_folder: Works with `.node-version` and `.nvmrc` files
## Installation
### Using a script (macOS/Linux)
For `bash`, `zsh` and `fish` shells, there's an [automatic installation script](./.ci/install.sh):
For `bash`, `zsh` and `fish` shells, there's an [automatic installation script](./.ci/install.sh).
First ensure that `curl` and `unzip` are already installed on you operating system. Then execute:
```sh
curl -fsSL https://fnm.vercel.app/install | bash
@ -118,31 +120,41 @@ Please follow your shell instructions to install them. @@ -118,31 +120,41 @@ Please follow your shell instructions to install them.
### Shell Setup
fnm needs to run some shell commands before you can start using it.
This is done by evaluating the output of `fnm env`. Check out the following guides for the shell you use:
Environment variables need to be setup before you can start using fnm.
This is done by evaluating the output of `fnm env`.
To automatically run `fnm use` when a directory contains a `.node-version` or `.nvmrc` file, add the `--use-on-cd` option to your shell setup.
Adding a `.node-version` to your project is as simple as:
```bash
$ node --version
v14.18.3
$ node --version > .node-version
```
Check out the following guides for the shell you use:
#### Bash
add the following to your `.bashrc` profile:
Add the following to your `.bashrc` profile:
```bash
eval "$(fnm env)"
eval "$(fnm env --use-on-cd)"
```
#### Zsh
add the following to your `.zshrc` profile:
Add the following to your `.zshrc` profile:
```zsh
eval "$(fnm env)"
eval "$(fnm env --use-on-cd)"
```
#### Fish shell
create `~/.config/fish/conf.d/fnm.fish` add this line to it:
Create `~/.config/fish/conf.d/fnm.fish` add this line to it:
```fish
fnm env | source
fnm env --use-on-cd | source
```
#### PowerShell

802
docs/commands.md

File diff suppressed because it is too large Load Diff

2
docs/fnm.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

269
e2e/__snapshots__/aliases.test.ts.snap

@ -0,0 +1,269 @@ @@ -0,0 +1,269 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash aliasing versions: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install 6.11.3
fnm install 8.11.3
fnm alias 8.11 oldie
fnm alias 6 older
fnm default older
((fnm ls) | grep 8.11.3 || (echo "Expected output to contain 8.11.3" && exit 1)) | grep oldie || (echo "Expected output to contain oldie" && exit 1)
((fnm ls) | grep 6.11.3 || (echo "Expected output to contain 6.11.3" && exit 1)) | grep older || (echo "Expected output to contain older" && exit 1)
fnm use older
if [ "$(node --version)" != "v6.11.3" ]; then
echo "Expected node version to be v6.11.3. Got $(node --version)"
exit 1
fi
fnm use oldie
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi
fnm use default
if [ "$(node --version)" != "v6.11.3" ]; then
echo "Expected node version to be v6.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash allows to install an lts if version missing: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm use --install-if-missing
(fnm ls) | grep lts-latest || (echo "Expected output to contain lts-latest" && exit 1)"
`;
exports[`Bash can alias the system node: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm alias system my_system
(fnm ls) | grep my_system || (echo "Expected output to contain my_system" && exit 1)
fnm alias system default
fnm alias my_system my_system2
(fnm ls) | grep my_system2 || (echo "Expected output to contain my_system2" && exit 1)
(fnm use my_system) | grep 'Bypassing fnm' || (echo "Expected output to contain 'Bypassing fnm'" && exit 1)
fnm unalias my_system
(fnm use my_system 2>&1) | grep 'Requested version my_system is not currently installed' || (echo "Expected output to contain 'Requested version my_system is not currently installed'" && exit 1)"
`;
exports[`Bash errors when alias is not found: Bash 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
(fnm use 2>&1) | grep 'Requested version lts-latest is not' || (echo "Expected output to contain 'Requested version lts-latest is not'" && exit 1)"
`;
exports[`Bash unalias a version: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install 11.10.0
fnm install 8.11.3
fnm alias 8.11.3 version8
(fnm ls) | grep version8 || (echo "Expected output to contain version8" && exit 1)
fnm unalias version8
(fnm use version8 2>&1) | grep 'Requested version version8 is not currently installed' || (echo "Expected output to contain 'Requested version version8 is not currently installed'" && exit 1)"
`;
exports[`Bash unalias errors if alias not found: Bash 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
(fnm unalias lts 2>&1) | grep 'Requested alias lts not found' || (echo "Expected output to contain 'Requested alias lts not found'" && exit 1)"
`;
exports[`Fish aliasing versions: Fish 1`] = `
"fnm env | source
fnm install 6.11.3
fnm install 8.11.3
fnm alias 8.11 oldie
fnm alias 6 older
fnm default older
begin; begin; fnm ls; end | grep 8.11.3; or echo "Expected output to contain 8.11.3" && exit 1; end | grep oldie; or echo "Expected output to contain oldie" && exit 1
begin; begin; fnm ls; end | grep 6.11.3; or echo "Expected output to contain 6.11.3" && exit 1; end | grep older; or echo "Expected output to contain older" && exit 1
fnm use older
set ____test____ (node --version)
if test "$____test____" != "v6.11.3"
echo "Expected node version to be v6.11.3. Got $____test____"
exit 1
end
fnm use oldie
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end
fnm use default
set ____test____ (node --version)
if test "$____test____" != "v6.11.3"
echo "Expected node version to be v6.11.3. Got $____test____"
exit 1
end"
`;
exports[`Fish allows to install an lts if version missing: Fish 1`] = `
"fnm env | source
fnm use --install-if-missing
begin; fnm ls; end | grep lts-latest; or echo "Expected output to contain lts-latest" && exit 1"
`;
exports[`Fish can alias the system node: Fish 1`] = `
"fnm env | source
fnm alias system my_system
begin; fnm ls; end | grep my_system; or echo "Expected output to contain my_system" && exit 1
fnm alias system default
fnm alias my_system my_system2
begin; fnm ls; end | grep my_system2; or echo "Expected output to contain my_system2" && exit 1
begin; fnm use my_system; end | grep 'Bypassing fnm'; or echo "Expected output to contain 'Bypassing fnm'" && exit 1
fnm unalias my_system
begin; fnm use my_system 2>&1; end | grep 'Requested version my_system is not currently installed'; or echo "Expected output to contain 'Requested version my_system is not currently installed'" && exit 1"
`;
exports[`Fish errors when alias is not found: Fish 1`] = `
"fnm env --log-level=error | source
begin; fnm use 2>&1; end | grep 'Requested version lts-latest is not'; or echo "Expected output to contain 'Requested version lts-latest is not'" && exit 1"
`;
exports[`Fish unalias a version: Fish 1`] = `
"fnm env | source
fnm install 11.10.0
fnm install 8.11.3
fnm alias 8.11.3 version8
begin; fnm ls; end | grep version8; or echo "Expected output to contain version8" && exit 1
fnm unalias version8
begin; fnm use version8 2>&1; end | grep 'Requested version version8 is not currently installed'; or echo "Expected output to contain 'Requested version version8 is not currently installed'" && exit 1"
`;
exports[`Fish unalias errors if alias not found: Fish 1`] = `
"fnm env --log-level=error | source
begin; fnm unalias lts 2>&1; end | grep 'Requested alias lts not found'; or echo "Expected output to contain 'Requested alias lts not found'" && exit 1"
`;
exports[`PowerShell aliasing versions: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install 6.11.3
fnm install 8.11.3
fnm alias 8.11 oldie
fnm alias 6 older
fnm default older
$($__out__ = $($($__out__ = $(fnm ls | Select-String 8.11.3); if ($__out__ -eq $null) { exit 1 } else { $__out__ }) | Select-String oldie); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
$($__out__ = $($($__out__ = $(fnm ls | Select-String 6.11.3); if ($__out__ -eq $null) { exit 1 } else { $__out__ }) | Select-String older); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm use older
if ( "$(node --version)" -ne "v6.11.3" ) { echo "Expected node version to be v6.11.3. Got $(node --version)"; exit 1 }
fnm use oldie
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }
fnm use default
if ( "$(node --version)" -ne "v6.11.3" ) { echo "Expected node version to be v6.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell allows to install an lts if version missing: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm use --install-if-missing
$($__out__ = $(fnm ls | Select-String lts-latest); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`PowerShell can alias the system node: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm alias system my_system
$($__out__ = $(fnm ls | Select-String my_system); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm alias system default
fnm alias my_system my_system2
$($__out__ = $(fnm ls | Select-String my_system2); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
$($__out__ = $(fnm use my_system | Select-String 'Bypassing fnm'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm unalias my_system
$($__out__ = $(fnm use my_system 2>&1 | Select-String 'Requested version my_system is not currently installed'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`PowerShell errors when alias is not found: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --log-level=error | Out-String | Invoke-Expression
$($__out__ = $(fnm use 2>&1 | Select-String 'Requested version lts-latest is not'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`PowerShell unalias a version: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install 11.10.0
fnm install 8.11.3
fnm alias 8.11.3 version8
$($__out__ = $(fnm ls | Select-String version8); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm unalias version8
$($__out__ = $(fnm use version8 2>&1 | Select-String 'Requested version version8 is not currently installed'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`PowerShell unalias errors if alias not found: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --log-level=error | Out-String | Invoke-Expression
$($__out__ = $(fnm unalias lts 2>&1 | Select-String 'Requested alias lts not found'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`Zsh aliasing versions: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install 6.11.3
fnm install 8.11.3
fnm alias 8.11 oldie
fnm alias 6 older
fnm default older
((fnm ls) | grep 8.11.3 || (echo "Expected output to contain 8.11.3" && exit 1)) | grep oldie || (echo "Expected output to contain oldie" && exit 1)
((fnm ls) | grep 6.11.3 || (echo "Expected output to contain 6.11.3" && exit 1)) | grep older || (echo "Expected output to contain older" && exit 1)
fnm use older
if [ "$(node --version)" != "v6.11.3" ]; then
echo "Expected node version to be v6.11.3. Got $(node --version)"
exit 1
fi
fnm use oldie
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi
fnm use default
if [ "$(node --version)" != "v6.11.3" ]; then
echo "Expected node version to be v6.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh allows to install an lts if version missing: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm use --install-if-missing
(fnm ls) | grep lts-latest || (echo "Expected output to contain lts-latest" && exit 1)"
`;
exports[`Zsh can alias the system node: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm alias system my_system
(fnm ls) | grep my_system || (echo "Expected output to contain my_system" && exit 1)
fnm alias system default
fnm alias my_system my_system2
(fnm ls) | grep my_system2 || (echo "Expected output to contain my_system2" && exit 1)
(fnm use my_system) | grep 'Bypassing fnm' || (echo "Expected output to contain 'Bypassing fnm'" && exit 1)
fnm unalias my_system
(fnm use my_system 2>&1) | grep 'Requested version my_system is not currently installed' || (echo "Expected output to contain 'Requested version my_system is not currently installed'" && exit 1)"
`;
exports[`Zsh errors when alias is not found: Zsh 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
(fnm use 2>&1) | grep 'Requested version lts-latest is not' || (echo "Expected output to contain 'Requested version lts-latest is not'" && exit 1)"
`;
exports[`Zsh unalias a version: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install 11.10.0
fnm install 8.11.3
fnm alias 8.11.3 version8
(fnm ls) | grep version8 || (echo "Expected output to contain version8" && exit 1)
fnm unalias version8
(fnm use version8 2>&1) | grep 'Requested version version8 is not currently installed' || (echo "Expected output to contain 'Requested version version8 is not currently installed'" && exit 1)"
`;
exports[`Zsh unalias errors if alias not found: Zsh 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
(fnm unalias lts 2>&1) | grep 'Requested alias lts not found' || (echo "Expected output to contain 'Requested alias lts not found'" && exit 1)"
`;

284
e2e/__snapshots__/basic.test.ts.snap

@ -0,0 +1,284 @@ @@ -0,0 +1,284 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash .node-version: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash .nvmrc: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash \`fnm ls\` with nothing installed: Bash 1`] = `
"set -e
eval "$(fnm env)"
if [ "$(fnm ls)" != "* system" ]; then
echo "Expected fnm ls to be * system. Got $(fnm ls)"
exit 1
fi"
`;
exports[`Bash basic usage: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
fnm use v8.11.3
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash resolves partial semver: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install 6
fnm use 6
if [ "$(node --version)" != "v6.17.1" ]; then
echo "Expected node version to be v6.17.1. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash use on cd: Bash 1`] = `
"set -e
eval "$(fnm env --use-on-cd)"
fnm install v8.11.3
fnm install v12.22.12
cd subdir
if [ "$(node --version)" != "v12.22.12" ]; then
echo "Expected node version to be v12.22.12. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash when .node-version and .nvmrc are in sync, it throws no error: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v11.10.0" ]; then
echo "Expected node version to be v11.10.0. Got $(node --version)"
exit 1
fi"
`;
exports[`Fish .node-version: Fish 1`] = `
"fnm env | source
fnm install
fnm use
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end"
`;
exports[`Fish .nvmrc: Fish 1`] = `
"fnm env | source
fnm install
fnm use
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end"
`;
exports[`Fish \`fnm ls\` with nothing installed: Fish 1`] = `
"fnm env | source
set ____test____ (fnm ls)
if test "$____test____" != "* system"
echo "Expected fnm ls to be * system. Got $____test____"
exit 1
end"
`;
exports[`Fish basic usage: Fish 1`] = `
"fnm env | source
fnm install v8.11.3
fnm use v8.11.3
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end"
`;
exports[`Fish resolves partial semver: Fish 1`] = `
"fnm env | source
fnm install 6
fnm use 6
set ____test____ (node --version)
if test "$____test____" != "v6.17.1"
echo "Expected node version to be v6.17.1. Got $____test____"
exit 1
end"
`;
exports[`Fish use on cd: Fish 1`] = `
"fnm env --use-on-cd | source
fnm install v8.11.3
fnm install v12.22.12
cd subdir
set ____test____ (node --version)
if test "$____test____" != "v12.22.12"
echo "Expected node version to be v12.22.12. Got $____test____"
exit 1
end"
`;
exports[`Fish when .node-version and .nvmrc are in sync, it throws no error: Fish 1`] = `
"fnm env | source
fnm install
fnm use
set ____test____ (node --version)
if test "$____test____" != "v11.10.0"
echo "Expected node version to be v11.10.0. Got $____test____"
exit 1
end"
`;
exports[`PowerShell .node-version: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install
fnm use
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell .nvmrc: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install
fnm use
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell \`fnm ls\` with nothing installed: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
if ( "$(fnm ls)" -ne "* system" ) { echo "Expected fnm ls to be * system. Got $(fnm ls)"; exit 1 }"
`;
exports[`PowerShell basic usage: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install v8.11.3
fnm use v8.11.3
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell resolves partial semver: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install 6
fnm use 6
if ( "$(node --version)" -ne "v6.17.1" ) { echo "Expected node version to be v6.17.1. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell use on cd: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --use-on-cd | Out-String | Invoke-Expression
fnm install v8.11.3
fnm install v12.22.12
cd subdir
if ( "$(node --version)" -ne "v12.22.12" ) { echo "Expected node version to be v12.22.12. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell when .node-version and .nvmrc are in sync, it throws no error: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install
fnm use
if ( "$(node --version)" -ne "v11.10.0" ) { echo "Expected node version to be v11.10.0. Got $(node --version)"; exit 1 }"
`;
exports[`Zsh .node-version: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh .nvmrc: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh \`fnm ls\` with nothing installed: Zsh 1`] = `
"set -e
eval "$(fnm env)"
if [ "$(fnm ls)" != "* system" ]; then
echo "Expected fnm ls to be * system. Got $(fnm ls)"
exit 1
fi"
`;
exports[`Zsh basic usage: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
fnm use v8.11.3
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh resolves partial semver: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install 6
fnm use 6
if [ "$(node --version)" != "v6.17.1" ]; then
echo "Expected node version to be v6.17.1. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh use on cd: Zsh 1`] = `
"set -e
eval "$(fnm env --use-on-cd)"
fnm install v8.11.3
fnm install v12.22.12
cd subdir
if [ "$(node --version)" != "v12.22.12" ]; then
echo "Expected node version to be v12.22.12. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh when .node-version and .nvmrc are in sync, it throws no error: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
if [ "$(node --version)" != "v11.10.0" ]; then
echo "Expected node version to be v11.10.0. Got $(node --version)"
exit 1
fi"
`;

96
e2e/__snapshots__/current.test.ts.snap

@ -0,0 +1,96 @@ @@ -0,0 +1,96 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash current returns the current Node.js version set in fnm: Bash 1`] = `
"set -e
eval "$(fnm env)"
if [ "$(fnm current)" != "none" ]; then
echo "Expected currently activated version to be none. Got $(fnm current)"
exit 1
fi
fnm install v8.11.3
fnm install v10.10.0
fnm use v8.11.3
if [ "$(fnm current)" != "v8.11.3" ]; then
echo "Expected currently activated version to be v8.11.3. Got $(fnm current)"
exit 1
fi
fnm use v10.10.0
if [ "$(fnm current)" != "v10.10.0" ]; then
echo "Expected currently activated version to be v10.10.0. Got $(fnm current)"
exit 1
fi
fnm use system
if [ "$(fnm current)" != "system" ]; then
echo "Expected currently activated version to be system. Got $(fnm current)"
exit 1
fi"
`;
exports[`Fish current returns the current Node.js version set in fnm: Fish 1`] = `
"fnm env | source
set ____test____ (fnm current)
if test "$____test____" != "none"
echo "Expected currently activated version to be none. Got $____test____"
exit 1
end
fnm install v8.11.3
fnm install v10.10.0
fnm use v8.11.3
set ____test____ (fnm current)
if test "$____test____" != "v8.11.3"
echo "Expected currently activated version to be v8.11.3. Got $____test____"
exit 1
end
fnm use v10.10.0
set ____test____ (fnm current)
if test "$____test____" != "v10.10.0"
echo "Expected currently activated version to be v10.10.0. Got $____test____"
exit 1
end
fnm use system
set ____test____ (fnm current)
if test "$____test____" != "system"
echo "Expected currently activated version to be system. Got $____test____"
exit 1
end"
`;
exports[`PowerShell current returns the current Node.js version set in fnm: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
if ( "$(fnm current)" -ne "none" ) { echo "Expected currently activated version to be none. Got $(fnm current)"; exit 1 }
fnm install v8.11.3
fnm install v10.10.0
fnm use v8.11.3
if ( "$(fnm current)" -ne "v8.11.3" ) { echo "Expected currently activated version to be v8.11.3. Got $(fnm current)"; exit 1 }
fnm use v10.10.0
if ( "$(fnm current)" -ne "v10.10.0" ) { echo "Expected currently activated version to be v10.10.0. Got $(fnm current)"; exit 1 }
fnm use system
if ( "$(fnm current)" -ne "system" ) { echo "Expected currently activated version to be system. Got $(fnm current)"; exit 1 }"
`;
exports[`Zsh current returns the current Node.js version set in fnm: Zsh 1`] = `
"set -e
eval "$(fnm env)"
if [ "$(fnm current)" != "none" ]; then
echo "Expected currently activated version to be none. Got $(fnm current)"
exit 1
fi
fnm install v8.11.3
fnm install v10.10.0
fnm use v8.11.3
if [ "$(fnm current)" != "v8.11.3" ]; then
echo "Expected currently activated version to be v8.11.3. Got $(fnm current)"
exit 1
fi
fnm use v10.10.0
if [ "$(fnm current)" != "v10.10.0" ]; then
echo "Expected currently activated version to be v10.10.0. Got $(fnm current)"
exit 1
fi
fnm use system
if [ "$(fnm current)" != "system" ]; then
echo "Expected currently activated version to be system. Got $(fnm current)"
exit 1
fi"
`;

18
e2e/__snapshots__/env.test.ts.snap

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash outputs json: Bash 1`] = `
"set -e
fnm env --json > file.json"
`;
exports[`Fish outputs json: Fish 1`] = `"fnm env --json > file.json"`;
exports[`PowerShell outputs json: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --json | Out-File file.json -Encoding UTF8"
`;
exports[`Zsh outputs json: Zsh 1`] = `
"set -e
fnm env --json > file.json"
`;

70
e2e/__snapshots__/exec.test.ts.snap

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash \`exec\` usage: Bash 1`] = `
"set -e
fnm install
fnm install v6.10.0
fnm install v10.10.0
if [ "$(fnm exec -- node -v)" != "v8.10.0" ]; then
echo "Expected version file exec to be v8.10.0. Got $(fnm exec -- node -v)"
exit 1
fi
if [ "$(fnm exec --using=6 -- node -v)" != "v6.10.0" ]; then
echo "Expected exec:6 node -v to be v6.10.0. Got $(fnm exec --using=6 -- node -v)"
exit 1
fi
if [ "$(fnm exec --using=10 -- node -v)" != "v10.10.0" ]; then
echo "Expected exec:6 node -v to be v10.10.0. Got $(fnm exec --using=10 -- node -v)"
exit 1
fi"
`;
exports[`Fish \`exec\` usage: Fish 1`] = `
"fnm install
fnm install v6.10.0
fnm install v10.10.0
set ____test____ (fnm exec -- node -v)
if test "$____test____" != "v8.10.0"
echo "Expected version file exec to be v8.10.0. Got $____test____"
exit 1
end
set ____test____ (fnm exec --using=6 -- node -v)
if test "$____test____" != "v6.10.0"
echo "Expected exec:6 node -v to be v6.10.0. Got $____test____"
exit 1
end
set ____test____ (fnm exec --using=10 -- node -v)
if test "$____test____" != "v10.10.0"
echo "Expected exec:6 node -v to be v10.10.0. Got $____test____"
exit 1
end"
`;
exports[`PowerShell \`exec\` usage: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm install
fnm install v6.10.0
fnm install v10.10.0
if ( "$(fnm exec -- node -v)" -ne "v8.10.0" ) { echo "Expected version file exec to be v8.10.0. Got $(fnm exec -- node -v)"; exit 1 }
if ( "$(fnm exec --using=6 -- node -v)" -ne "v6.10.0" ) { echo "Expected exec:6 node -v to be v6.10.0. Got $(fnm exec --using=6 -- node -v)"; exit 1 }
if ( "$(fnm exec --using=10 -- node -v)" -ne "v10.10.0" ) { echo "Expected exec:6 node -v to be v10.10.0. Got $(fnm exec --using=10 -- node -v)"; exit 1 }"
`;
exports[`Zsh \`exec\` usage: Zsh 1`] = `
"set -e
fnm install
fnm install v6.10.0
fnm install v10.10.0
if [ "$(fnm exec -- node -v)" != "v8.10.0" ]; then
echo "Expected version file exec to be v8.10.0. Got $(fnm exec -- node -v)"
exit 1
fi
if [ "$(fnm exec --using=6 -- node -v)" != "v6.10.0" ]; then
echo "Expected exec:6 node -v to be v6.10.0. Got $(fnm exec --using=6 -- node -v)"
exit 1
fi
if [ "$(fnm exec --using=10 -- node -v)" != "v10.10.0" ]; then
echo "Expected exec:6 node -v to be v10.10.0. Got $(fnm exec --using=10 -- node -v)"
exit 1
fi"
`;

28
e2e/__snapshots__/existing-installation.test.ts.snap

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash warns about an existing installation: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
(fnm install v8.11.3 2>&1) | grep 'already installed' || (echo "Expected output to contain 'already installed'" && exit 1)"
`;
exports[`Fish warns about an existing installation: Fish 1`] = `
"fnm env | source
fnm install v8.11.3
begin; fnm install v8.11.3 2>&1; end | grep 'already installed'; or echo "Expected output to contain 'already installed'" && exit 1"
`;
exports[`PowerShell warns about an existing installation: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install v8.11.3
$($__out__ = $(fnm install v8.11.3 2>&1 | Select-String 'already installed'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`Zsh warns about an existing installation: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
(fnm install v8.11.3 2>&1) | grep 'already installed' || (echo "Expected output to contain 'already installed'" && exit 1)"
`;

32
e2e/__snapshots__/latest-lts.test.ts.snap

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash installs latest lts: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install --lts
(fnm ls) | grep lts-latest || (echo "Expected output to contain lts-latest" && exit 1)
fnm use 'lts/*'"
`;
exports[`Fish installs latest lts: Fish 1`] = `
"fnm env | source
fnm install --lts
begin; fnm ls; end | grep lts-latest; or echo "Expected output to contain lts-latest" && exit 1
fnm use 'lts/*'"
`;
exports[`PowerShell installs latest lts: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install --lts
$($__out__ = $(fnm ls | Select-String lts-latest); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm use 'lts/*'"
`;
exports[`Zsh installs latest lts: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install --lts
(fnm ls) | grep lts-latest || (echo "Expected output to contain lts-latest" && exit 1)
fnm use 'lts/*'"
`;

127
e2e/__snapshots__/log-level.test.ts.snap

@ -0,0 +1,127 @@ @@ -0,0 +1,127 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash "quiet" log level: Bash 1`] = `
"set -e
eval "$(fnm env --log-level=quiet)"
if [ "$(fnm install v8.11.3)" != "" ]; then
echo "Expected fnm install to be . Got $(fnm install v8.11.3)"
exit 1
fi
if [ "$(fnm use v8.11.3)" != "" ]; then
echo "Expected fnm use to be . Got $(fnm use v8.11.3)"
exit 1
fi
if [ "$(fnm alias v8.11.3 something)" != "" ]; then
echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"
exit 1
fi"
`;
exports[`Bash error log level: Bash 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
if [ "$(fnm install v8.11.3)" != "" ]; then
echo "Expected fnm install to be . Got $(fnm install v8.11.3)"
exit 1
fi
if [ "$(fnm use v8.11.3)" != "" ]; then
echo "Expected fnm use to be . Got $(fnm use v8.11.3)"
exit 1
fi
if [ "$(fnm alias v8.11.3 something)" != "" ]; then
echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"
exit 1
fi
(fnm alias abcd efg 2>&1) | grep "find requested version" || (echo "Expected output to contain "find requested version"" && exit 1)"
`;
exports[`Fish "quiet" log level: Fish 1`] = `
"fnm env --log-level=quiet | source
set ____test____ (fnm install v8.11.3)
if test "$____test____" != ""
echo "Expected fnm install to be . Got $____test____"
exit 1
end
set ____test____ (fnm use v8.11.3)
if test "$____test____" != ""
echo "Expected fnm use to be . Got $____test____"
exit 1
end
set ____test____ (fnm alias v8.11.3 something)
if test "$____test____" != ""
echo "Expected fnm alias to be . Got $____test____"
exit 1
end"
`;
exports[`Fish error log level: Fish 1`] = `
"fnm env --log-level=error | source
set ____test____ (fnm install v8.11.3)
if test "$____test____" != ""
echo "Expected fnm install to be . Got $____test____"
exit 1
end
set ____test____ (fnm use v8.11.3)
if test "$____test____" != ""
echo "Expected fnm use to be . Got $____test____"
exit 1
end
set ____test____ (fnm alias v8.11.3 something)
if test "$____test____" != ""
echo "Expected fnm alias to be . Got $____test____"
exit 1
end
begin; fnm alias abcd efg 2>&1; end | grep "find requested version"; or echo "Expected output to contain "find requested version"" && exit 1"
`;
exports[`PowerShell "quiet" log level: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --log-level=quiet | Out-String | Invoke-Expression
if ( "$(fnm install v8.11.3)" -ne "" ) { echo "Expected fnm install to be . Got $(fnm install v8.11.3)"; exit 1 }
if ( "$(fnm use v8.11.3)" -ne "" ) { echo "Expected fnm use to be . Got $(fnm use v8.11.3)"; exit 1 }
if ( "$(fnm alias v8.11.3 something)" -ne "" ) { echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"; exit 1 }"
`;
exports[`PowerShell error log level: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --log-level=error | Out-String | Invoke-Expression
if ( "$(fnm install v8.11.3)" -ne "" ) { echo "Expected fnm install to be . Got $(fnm install v8.11.3)"; exit 1 }
if ( "$(fnm use v8.11.3)" -ne "" ) { echo "Expected fnm use to be . Got $(fnm use v8.11.3)"; exit 1 }
if ( "$(fnm alias v8.11.3 something)" -ne "" ) { echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"; exit 1 }
$($__out__ = $(fnm alias abcd efg 2>&1 | Select-String "find requested version"); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`Zsh "quiet" log level: Zsh 1`] = `
"set -e
eval "$(fnm env --log-level=quiet)"
if [ "$(fnm install v8.11.3)" != "" ]; then
echo "Expected fnm install to be . Got $(fnm install v8.11.3)"
exit 1
fi
if [ "$(fnm use v8.11.3)" != "" ]; then
echo "Expected fnm use to be . Got $(fnm use v8.11.3)"
exit 1
fi
if [ "$(fnm alias v8.11.3 something)" != "" ]; then
echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"
exit 1
fi"
`;
exports[`Zsh error log level: Zsh 1`] = `
"set -e
eval "$(fnm env --log-level=error)"
if [ "$(fnm install v8.11.3)" != "" ]; then
echo "Expected fnm install to be . Got $(fnm install v8.11.3)"
exit 1
fi
if [ "$(fnm use v8.11.3)" != "" ]; then
echo "Expected fnm use to be . Got $(fnm use v8.11.3)"
exit 1
fi
if [ "$(fnm alias v8.11.3 something)" != "" ]; then
echo "Expected fnm alias to be . Got $(fnm alias v8.11.3 something)"
exit 1
fi
(fnm alias abcd efg 2>&1) | grep "find requested version" || (echo "Expected output to contain "find requested version"" && exit 1)"
`;

67
e2e/__snapshots__/multishell.test.ts.snap

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash multishell changes don't affect parent: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
fnm install v11.9.0
echo 'set -e
eval "$(fnm env)"
fnm use v11
if [ "$(node --version)" != "v11.9.0" ]; then
echo "Expected node version to be v11.9.0. Got $(node --version)"
exit 1
fi' | bash
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Fish multishell changes don't affect parent: Fish 1`] = `
"fnm env | source
fnm install v8.11.3
fnm install v11.9.0
fish -c 'fnm env | source
fnm use v11
set ____test____ (node --version)
if test "$____test____" != "v11.9.0"
echo "Expected node version to be v11.9.0. Got $____test____"
exit 1
end'
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end"
`;
exports[`PowerShell multishell changes don't affect parent: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install v8.11.3
fnm install v11.9.0
echo '$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm use v11
if ( "$(node --version)" -ne "v11.9.0" ) { echo "Expected node version to be v11.9.0. Got $(node --version)"; exit 1 }' | pwsh -NoProfile
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`Zsh multishell changes don't affect parent: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install v8.11.3
fnm install v11.9.0
echo 'set -e
eval "$(fnm env)"
fnm use v11
if [ "$(node --version)" != "v11.9.0" ]; then
echo "Expected node version to be v11.9.0. Got $(node --version)"
exit 1
fi' | zsh
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;

32
e2e/__snapshots__/nvmrc-lts.test.ts.snap

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash uses .nvmrc with lts definition: Bash 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
(fnm ls) | grep lts-dubnium || (echo "Expected output to contain lts-dubnium" && exit 1)"
`;
exports[`Fish uses .nvmrc with lts definition: Fish 1`] = `
"fnm env | source
fnm install
fnm use
begin; fnm ls; end | grep lts-dubnium; or echo "Expected output to contain lts-dubnium" && exit 1"
`;
exports[`PowerShell uses .nvmrc with lts definition: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install
fnm use
$($__out__ = $(fnm ls | Select-String lts-dubnium); if ($__out__ -eq $null) { exit 1 } else { $__out__ })"
`;
exports[`Zsh uses .nvmrc with lts definition: Zsh 1`] = `
"set -e
eval "$(fnm env)"
fnm install
fnm use
(fnm ls) | grep lts-dubnium || (echo "Expected output to contain lts-dubnium" && exit 1)"
`;

59
e2e/__snapshots__/shims.test.ts.snap

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash outputs json: Bash 1`] = `
"set -e
fnm env --json --with-shims > file.json"
`;
exports[`Bash runs Node through a shim: Bash 1`] = `
"set -e
eval "$(fnm env --with-shims)"
fnm install 12.0.0
fnm use 12.0.0
if [ "$(node --version)" != "v12.0.0" ]; then
echo "Expected node version to be v12.0.0. Got $(node --version)"
exit 1
fi"
`;
exports[`Fish outputs json: Fish 1`] = `"fnm env --json --with-shims > file.json"`;
exports[`Fish runs Node through a shim: Fish 1`] = `
"fnm env --with-shims | source
fnm install 12.0.0
fnm use 12.0.0
set ____test____ (node --version)
if test "$____test____" != "v12.0.0"
echo "Expected node version to be v12.0.0. Got $____test____"
exit 1
end"
`;
exports[`PowerShell outputs json: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --json --with-shims | Out-File file.json -Encoding UTF8"
`;
exports[`PowerShell runs Node through a shim: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --with-shims | Out-String | Invoke-Expression
fnm install 12.0.0
fnm use 12.0.0
if ( "$(node --version)" -ne "v12.0.0" ) { echo "Expected node version to be v12.0.0. Got $(node --version)"; exit 1 }"
`;
exports[`Zsh outputs json: Zsh 1`] = `
"set -e
fnm env --json --with-shims > file.json"
`;
exports[`Zsh runs Node through a shim: Zsh 1`] = `
"set -e
eval "$(fnm env --with-shims)"
fnm install 12.0.0
fnm use 12.0.0
if [ "$(node --version)" != "v12.0.0" ]; then
echo "Expected node version to be v12.0.0. Got $(node --version)"
exit 1
fi"
`;

46
e2e/__snapshots__/uninstall.test.ts.snap

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bash uninstalls a version: Bash 1`] = `
"set -e
fnm install 12.0.0
fnm alias 12.0.0 hello
((fnm ls) | grep v12.0.0 || (echo "Expected output to contain v12.0.0" && exit 1)) | grep hello || (echo "Expected output to contain hello" && exit 1)
fnm uninstall hello
if [ "$(fnm ls)" != "* system" ]; then
echo "Expected fnm ls to be * system. Got $(fnm ls)"
exit 1
fi"
`;
exports[`Fish uninstalls a version: Fish 1`] = `
"fnm install 12.0.0
fnm alias 12.0.0 hello
begin; begin; fnm ls; end | grep v12.0.0; or echo "Expected output to contain v12.0.0" && exit 1; end | grep hello; or echo "Expected output to contain hello" && exit 1
fnm uninstall hello
set ____test____ (fnm ls)
if test "$____test____" != "* system"
echo "Expected fnm ls to be * system. Got $____test____"
exit 1
end"
`;
exports[`PowerShell uninstalls a version: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm install 12.0.0
fnm alias 12.0.0 hello
$($__out__ = $($($__out__ = $(fnm ls | Select-String v12.0.0); if ($__out__ -eq $null) { exit 1 } else { $__out__ }) | Select-String hello); if ($__out__ -eq $null) { exit 1 } else { $__out__ })
fnm uninstall hello
if ( "$(fnm ls)" -ne "* system" ) { echo "Expected fnm ls to be * system. Got $(fnm ls)"; exit 1 }"
`;
exports[`Zsh uninstalls a version: Zsh 1`] = `
"set -e
fnm install 12.0.0
fnm alias 12.0.0 hello
((fnm ls) | grep v12.0.0 || (echo "Expected output to contain v12.0.0" && exit 1)) | grep hello || (echo "Expected output to contain hello" && exit 1)
fnm uninstall hello
if [ "$(fnm ls)" != "* system" ]; then
echo "Expected fnm ls to be * system. Got $(fnm ls)"
exit 1
fi"
`;

129
e2e/aliases.test.ts

@ -0,0 +1,129 @@ @@ -0,0 +1,129 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
import { writeFile } from "node:fs/promises"
import path from "node:path"
import testCwd from "./shellcode/test-cwd.js"
import getStderr from "./shellcode/get-stderr.js"
import testNodeVersion from "./shellcode/test-node-version.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`allows to install an lts if version missing`, async () => {
await writeFile(path.join(testCwd(), ".node-version"), "lts/*")
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["use", "--install-if-missing"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "lts-latest")
)
.takeSnapshot(shell)
.execute(shell)
})
test(`errors when alias is not found`, async () => {
await writeFile(path.join(testCwd(), ".node-version"), "lts/*")
await script(shell)
.then(shell.env({ logLevel: "error" }))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["use"])),
"'Requested version lts-latest is not'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`unalias a version`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "11.10.0"]))
.then(shell.call("fnm", ["install", "8.11.3"]))
.then(shell.call("fnm", ["alias", "8.11.3", "version8"]))
.then(shell.scriptOutputContains(shell.call("fnm", ["ls"]), "version8"))
.then(shell.call("fnm", ["unalias", "version8"]))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["use", "version8"])),
"'Requested version version8 is not currently installed'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`unalias errors if alias not found`, async () => {
await script(shell)
.then(shell.env({ logLevel: "error" }))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["unalias", "lts"])),
"'Requested alias lts not found'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`can alias the system node`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["alias", "system", "my_system"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "my_system")
)
.then(shell.call("fnm", ["alias", "system", "default"]))
.then(shell.call("fnm", ["alias", "my_system", "my_system2"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "my_system2")
)
.then(
shell.scriptOutputContains(
shell.call("fnm", ["use", "my_system"]),
"'Bypassing fnm'"
)
)
.then(shell.call("fnm", ["unalias", "my_system"]))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["use", "my_system"])),
"'Requested version my_system is not currently installed'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`aliasing versions`, async () => {
const installedVersions = shell.call("fnm", ["ls"])
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "6.11.3"]))
.then(shell.call("fnm", ["install", "8.11.3"]))
.then(shell.call("fnm", ["alias", "8.11", "oldie"]))
.then(shell.call("fnm", ["alias", "6", "older"]))
.then(shell.call("fnm", ["default", "older"]))
.then(
shell.scriptOutputContains(
shell.scriptOutputContains(installedVersions, "8.11.3"),
"oldie"
)
)
.then(
shell.scriptOutputContains(
shell.scriptOutputContains(installedVersions, "6.11.3"),
"older"
)
)
.then(shell.call("fnm", ["use", "older"]))
.then(testNodeVersion(shell, "v6.11.3"))
.then(shell.call("fnm", ["use", "oldie"]))
.then(testNodeVersion(shell, "v8.11.3"))
.then(shell.call("fnm", ["use", "default"]))
.then(testNodeVersion(shell, "v6.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
})
}

93
e2e/basic.test.ts

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
import { writeFile, mkdir } from "node:fs/promises"
import { join } from "node:path"
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import testCwd from "./shellcode/test-cwd.js"
import testNodeVersion from "./shellcode/test-node-version.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test(`basic usage`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(shell.call("fnm", ["use", "v8.11.3"]))
.then(testNodeVersion(shell, "v8.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
test(`.nvmrc`, async () => {
await writeFile(join(testCwd(), ".nvmrc"), "v8.11.3")
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(testNodeVersion(shell, "v8.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
test(`.node-version`, async () => {
await writeFile(join(testCwd(), ".node-version"), "v8.11.3")
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(testNodeVersion(shell, "v8.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
test(`use on cd`, async () => {
await mkdir(join(testCwd(), "subdir"), { recursive: true })
await writeFile(join(testCwd(), "subdir", ".node-version"), "v12.22.12")
await script(shell)
.then(shell.env({ useOnCd: true }))
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(shell.call("fnm", ["install", "v12.22.12"]))
.then(shell.call("cd", ["subdir"]))
.then(testNodeVersion(shell, "v12.22.12"))
.takeSnapshot(shell)
.execute(shell)
})
test(`resolves partial semver`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "6"]))
.then(shell.call("fnm", ["use", "6"]))
.then(testNodeVersion(shell, "v6.17.1"))
.takeSnapshot(shell)
.execute(shell)
})
test("`fnm ls` with nothing installed", async () => {
await script(shell)
.then(shell.env({}))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["ls"]),
"* system",
"fnm ls"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test(`when .node-version and .nvmrc are in sync, it throws no error`, async () => {
await writeFile(join(testCwd(), ".nvmrc"), "v11.10.0")
await writeFile(join(testCwd(), ".node-version"), "v11.10.0")
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(testNodeVersion(shell, "v11.10.0"))
.takeSnapshot(shell)
.execute(shell)
})
})
}

47
e2e/current.test.ts

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test(`current returns the current Node.js version set in fnm`, async () => {
await script(shell)
.then(shell.env({}))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["current"]),
"none",
"currently activated version"
)
)
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(shell.call("fnm", ["install", "v10.10.0"]))
.then(shell.call("fnm", ["use", "v8.11.3"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["current"]),
"v8.11.3",
"currently activated version"
)
)
.then(shell.call("fnm", ["use", "v10.10.0"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["current"]),
"v10.10.0",
"currently activated version"
)
)
.then(shell.call("fnm", ["use", "system"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["current"]),
"system",
"currently activated version"
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

15
e2e/describe.ts

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
import { WinCmd } from "./shellcode/shells.js"
import { Shell } from "./shellcode/shells/types.js"
export default function describe(
shell: Pick<Shell, "name">,
fn: () => void
): void {
if (shell === WinCmd) {
// wincmd tests do not work right now and I don't have a Windows machine to fix it
// maybe in the future when I have some time to spin a VM to check what's happening.
return globalThis.describe.skip("WinCmd", fn)
}
globalThis.describe(shell.name(), fn)
}

35
e2e/env.test.ts

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
import { readFile } from "node:fs/promises"
import { join } from "node:path"
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import testCwd from "./shellcode/test-cwd.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test(`outputs json`, async () => {
const filename = `file.json`
await script(shell)
.then(
shell.redirectOutput(shell.call("fnm", ["env", "--json"]), {
output: filename,
})
)
.takeSnapshot(shell)
.execute(shell)
if (shell.currentlySupported()) {
const file = await readFile(join(testCwd(), filename), "utf8")
expect(JSON.parse(file)).toEqual({
FNM_ARCH: expect.any(String),
FNM_DIR: expect.any(String),
FNM_LOGLEVEL: "info",
FNM_MULTISHELL_PATH: expect.any(String),
FNM_NODE_DIST_MIRROR: expect.any(String),
FNM_VERSION_FILE_STRATEGY: "local",
FNM_VERSION_SWITCH_STRATEGY: "path-symlink",
})
}
})
})
}

42
e2e/exec.test.ts

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import testCwd from "./shellcode/test-cwd.js"
import fs from "node:fs/promises"
import path from "node:path"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test("`exec` usage", async () => {
await fs.writeFile(path.join(testCwd(), ".nvmrc"), "v8.10.0")
await script(shell)
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["install", "v6.10.0"]))
.then(shell.call("fnm", ["install", "v10.10.0"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["exec", "--", "node", "-v"]),
"v8.10.0",
"version file exec"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["exec", "--using=6", "--", "node", "-v"]),
"v6.10.0",
"exec:6 node -v"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["exec", "--using=10", "--", "node", "-v"]),
"v10.10.0",
"exec:6 node -v"
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

22
e2e/existing-installation.test.ts

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
import getStderr from "./shellcode/get-stderr.js"
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`warns about an existing installation`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["install", "v8.11.3"])),
"'already installed'"
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

19
e2e/latest-lts.test.ts

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`installs latest lts`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "--lts"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "lts-latest")
)
.then(shell.call("fnm", ["use", "'lts/*'"]))
.takeSnapshot(shell)
.execute(shell)
})
})
}

71
e2e/log-level.test.ts

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
import getStderr from "./shellcode/get-stderr.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`"quiet" log level`, async () => {
await script(shell)
.then(shell.env({ logLevel: "quiet" }))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["install", "v8.11.3"]),
"",
"fnm install"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["use", "v8.11.3"]),
"",
"fnm use"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["alias", "v8.11.3", "something"]),
"",
"fnm alias"
)
)
.takeSnapshot(shell)
.execute(shell)
})
test("error log level", async () => {
await script(shell)
.then(shell.env({ logLevel: "error" }))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["install", "v8.11.3"]),
"",
"fnm install"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["use", "v8.11.3"]),
"",
"fnm use"
)
)
.then(
shell.hasCommandOutput(
shell.call("fnm", ["alias", "v8.11.3", "something"]),
"",
"fnm alias"
)
)
.then(
shell.scriptOutputContains(
getStderr(shell.call("fnm", ["alias", "abcd", "efg"])),
`"find requested version"`
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

27
e2e/multishell.test.ts

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import testNodeVersion from "./shellcode/test-node-version.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`multishell changes don't affect parent`, async () => {
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "v8.11.3"]))
.then(shell.call("fnm", ["install", "v11.9.0"]))
.then(
shell.inSubShell(
script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["use", "v11"]))
.then(testNodeVersion(shell, "v11.9.0"))
.asLine()
)
)
.then(testNodeVersion(shell, "v8.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
})
}

24
e2e/nvmrc-lts.test.ts

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import fs from "node:fs/promises"
import path from "node:path"
import describe from "./describe.js"
import testCwd from "./shellcode/test-cwd.js"
for (const shell of [Bash, Fish, PowerShell, Zsh]) {
describe(shell, () => {
test(`uses .nvmrc with lts definition`, async () => {
await fs.writeFile(path.join(testCwd(), ".nvmrc"), `lts/dubnium`)
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "lts-dubnium")
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

3
e2e/shellcode/get-stderr.ts

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
export default function getStderr(script: string): string {
return `${script} 2>&1`
}

179
e2e/shellcode/script.ts

@ -0,0 +1,179 @@ @@ -0,0 +1,179 @@
import { ScriptLine, Shell } from "./shells/types.js"
import { execa, type ExecaChildProcess } from "execa"
import testTmpDir from "./test-tmp-dir.js"
import { Writable } from "node:stream"
import { dedent } from "ts-dedent"
import testCwd from "./test-cwd.js"
import path, { join } from "node:path"
import { writeFile } from "node:fs/promises"
import chalk from "chalk"
import testBinDir from "./test-bin-dir.js"
class Script {
constructor(
private readonly config: {
fnmDir: string
},
private readonly lines: ScriptLine[]
) {}
then(line: ScriptLine): Script {
return new Script(this.config, [...this.lines, line])
}
takeSnapshot(shell: Pick<Shell, "name">): this {
const script = this.lines.join("\n")
expect(script).toMatchSnapshot(shell.name())
return this
}
async execute(
shell: Pick<
Shell,
"binaryName" | "launchArgs" | "currentlySupported" | "forceFile"
>
): Promise<void> {
if (!shell.currentlySupported()) {
return
}
const args = [...shell.launchArgs()]
if (shell.forceFile) {
let filename = join(testTmpDir(), "script")
if (typeof shell.forceFile === "string") {
filename = filename + shell.forceFile
}
await writeFile(filename, [...this.lines, "exit 0"].join("\n"))
args.push(filename)
}
const child = execa(shell.binaryName(), args, {
stdio: [shell.forceFile ? "ignore" : "pipe", "pipe", "pipe"],
cwd: testCwd(),
env: (() => {
const newProcessEnv: Record<string, string> = {
...removeAllFnmEnvVars(process.env),
PATH: [testBinDir(), fnmTargetDir(), process.env.PATH]
.filter(Boolean)
.join(path.delimiter),
FNM_DIR: this.config.fnmDir,
}
delete newProcessEnv.NODE_OPTIONS
return newProcessEnv
})(),
extendEnv: false,
reject: false,
})
if (child.stdin) {
const childStdin = child.stdin
for (const line of this.lines) {
await write(childStdin, `${line}\n`)
}
await write(childStdin, "exit 0\n")
}
const { stdout, stderr } = streamOutputsAndBuffer(child)
const finished = await child
if (finished.failed) {
console.error(
dedent`
Script failed.
code ${finished.exitCode}
signal ${finished.signal}
stdout:
${padAllLines(stdout.join(""), 2)}
stderr:
${padAllLines(stderr.join(""), 2)}
`
)
throw new Error(
`Script failed on ${testCwd()} with code ${finished.exitCode}`
)
}
}
asLine(): ScriptLine {
return this.lines.join("\n")
}
}
function streamOutputsAndBuffer(child: ExecaChildProcess) {
const stdout: string[] = []
const stderr: string[] = []
const testName = expect.getState().currentTestName ?? "unknown"
const testPath = expect.getState().testPath ?? "unknown"
const stdoutPrefix = chalk.cyan.dim(`[stdout] ${testPath}/${testName}: `)
const stderrPrefix = chalk.magenta.dim(`[stderr] ${testPath}/${testName}: `)
if (child.stdout) {
child.stdout.on("data", (data) => {
const line = data.toString().trim()
if (line) {
process.stdout.write(`${stdoutPrefix}${line}\n`)
}
stdout.push(data.toString())
})
}
if (child.stderr) {
child.stderr.on("data", (data) => {
const line = data.toString().trim()
if (line) {
process.stdout.write(`${stderrPrefix}${line}\n`)
}
stderr.push(data.toString())
})
}
return { stdout, stderr }
}
function padAllLines(text: string, padding: number): string {
return text
.split("\n")
.map((line) => " ".repeat(padding) + line)
.join("\n")
}
function write(writable: Writable, text: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
writable.write(text, (err) => {
if (err) return reject(err)
return resolve()
})
})
}
export function script(shell: Pick<Shell, "dieOnErrors">): Script {
const fnmDir = path.join(testTmpDir(), "fnm")
return new Script({ fnmDir }, shell.dieOnErrors ? [shell.dieOnErrors()] : [])
}
function removeAllFnmEnvVars(obj: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const result: NodeJS.ProcessEnv = {}
for (const [key, value] of Object.entries(obj)) {
if (!key.startsWith("FNM_")) {
result[key] = value
}
}
return result
}
function fnmTargetDir(): string {
return path.join(
process.cwd(),
"target",
process.env.FNM_TARGET_NAME ?? "debug"
)
}

97
e2e/shellcode/shells.ts

@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
import { cmdCall } from "./shells/cmdCall.js"
import { cmdEnv } from "./shells/cmdEnv.js"
import { cmdExpectCommandOutput } from "./shells/expect-command-output.js"
import { cmdHasOutputContains } from "./shells/output-contains.js"
import { redirectOutput } from "./shells/redirect-output.js"
import { cmdInSubShell } from "./shells/sub-shell.js"
import { define, Shell } from "./shells/types.js"
export const Bash = {
...define<Shell>({
binaryName: () => "bash",
currentlySupported: () => true,
name: () => "Bash",
launchArgs: () => ["-i"],
escapeText: (x) => x,
dieOnErrors: () => `set -e`,
}),
...cmdEnv.bash,
...cmdCall.all,
...redirectOutput.bash,
...cmdExpectCommandOutput.bash,
...cmdHasOutputContains.bash,
...cmdInSubShell.bash,
}
export const Zsh = {
...define<Shell>({
binaryName: () => "zsh",
currentlySupported: () => process.platform !== "win32",
name: () => "Zsh",
launchArgs: () => [],
escapeText: (x) => x,
dieOnErrors: () => `set -e`,
}),
...cmdEnv.bash,
...cmdCall.all,
...redirectOutput.bash,
...cmdExpectCommandOutput.bash,
...cmdHasOutputContains.bash,
...cmdInSubShell.zsh,
}
export const Fish = {
...define<Shell>({
binaryName: () => "fish",
currentlySupported: () => process.platform !== "win32",
name: () => "Fish",
launchArgs: () => [],
escapeText: (x) => x,
forceFile: true,
}),
...cmdEnv.fish,
...cmdCall.all,
...redirectOutput.bash,
...cmdExpectCommandOutput.fish,
...cmdHasOutputContains.fish,
...cmdInSubShell.fish,
}
export const PowerShell = {
...define<Shell>({
binaryName: () => "pwsh",
forceFile: ".ps1",
currentlySupported: () => true,
name: () => "PowerShell",
launchArgs: () => ["-NoProfile"],
escapeText: (x) => x,
dieOnErrors: () => `$ErrorActionPreference = "Stop"`,
}),
...cmdEnv.powershell,
...cmdCall.all,
...redirectOutput.powershell,
...cmdExpectCommandOutput.powershell,
...cmdHasOutputContains.powershell,
...cmdInSubShell.powershell,
}
export const WinCmd = {
...define<Shell>({
binaryName: () => "cmd.exe",
currentlySupported: () => process.platform === "win32",
name: () => "Windows Command Prompt",
launchArgs: () => [],
escapeText: (str) =>
str
.replace(/\r/g, "")
.replace(/\n/g, "^\n\n")
.replace(/\%/g, "%%")
.replace(/\|/g, "^|")
.replace(/\(/g, "^(")
.replace(/\)/g, "^)"),
}),
...cmdEnv.wincmd,
...cmdCall.all,
...cmdExpectCommandOutput.wincmd,
...redirectOutput.bash,
}

12
e2e/shellcode/shells/cmdCall.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
import { define, ScriptLine } from "./types.js"
export type HasCall = {
call: (binary: string, args: string[]) => ScriptLine
}
export const cmdCall = {
all: define<HasCall>({
call: (binary: string, args: string[]) =>
`${binary} ${args.join(" ")}` as ScriptLine,
}),
}

33
e2e/shellcode/shells/cmdEnv.ts

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
import { ScriptLine, define } from "./types.js"
type EnvConfig = { useOnCd: boolean; logLevel: string; args: string[] }
export type HasEnv = { env(cfg: Partial<EnvConfig>): ScriptLine }
function stringify(envConfig: Partial<EnvConfig> = {}) {
const { useOnCd, logLevel, args } = envConfig
return [
`fnm env`,
useOnCd && "--use-on-cd",
logLevel && `--log-level=${logLevel}`,
args && args.join(" "),
]
.filter(Boolean)
.join(" ")
}
export const cmdEnv = {
bash: define<HasEnv>({
env: (envConfig) => `eval "$(${stringify(envConfig)})"`,
}),
fish: define<HasEnv>({
env: (envConfig) => `${stringify(envConfig)} | source`,
}),
powershell: define<HasEnv>({
env: (envConfig) =>
`${stringify(envConfig)} | Out-String | Invoke-Expression`,
}),
wincmd: define<HasEnv>({
env: (envConfig) =>
`FOR /f "tokens=*" %i IN ('${stringify(envConfig)}') DO CALL %i`,
}),
}

52
e2e/shellcode/shells/expect-command-output.ts

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
import { dedent } from "ts-dedent"
import { define, ScriptLine } from "./types.js"
export type HasExpectCommandOutput = {
hasCommandOutput(
script: ScriptLine,
output: string,
message: string
): ScriptLine
}
export const cmdExpectCommandOutput = {
bash: define<HasExpectCommandOutput>({
hasCommandOutput(script, output, message) {
return dedent`
if [ "$(${script})" != "${output}" ]; then
echo "Expected ${message} to be ${output}. Got $(${script})"
exit 1
fi
`
},
}),
fish: define<HasExpectCommandOutput>({
hasCommandOutput(script, output, message) {
return dedent`
set ____test____ (${script})
if test "$____test____" != "${output}"
echo "Expected ${message} to be ${output}. Got $____test____"
exit 1
end
`
},
}),
powershell: define<HasExpectCommandOutput>({
hasCommandOutput(script, output, message) {
return dedent`
if ( "$(${script})" -ne "${output}" ) { echo "Expected ${message} to be ${output}. Got $(${script})"; exit 1 }
`
},
}),
wincmd: define<HasExpectCommandOutput>({
hasCommandOutput(script, output, message) {
return dedent`
${script} | findstr ${output}
if %errorlevel% neq 0 (
echo Expected ${message} to be ${output}
exit 1
)
`
},
}),
}

24
e2e/shellcode/shells/output-contains.ts

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
import { define, ScriptLine } from "./types.js"
export type HasOutputContains = {
scriptOutputContains(script: ScriptLine, substring: string): ScriptLine
}
export const cmdHasOutputContains = {
bash: define<HasOutputContains>({
scriptOutputContains: (script, substring) => {
return `(${script}) | grep ${substring} || (echo "Expected output to contain ${substring}" && exit 1)`
},
}),
fish: define<HasOutputContains>({
scriptOutputContains: (script, substring) => {
return `begin; ${script}; end | grep ${substring}; or echo "Expected output to contain ${substring}" && exit 1`
},
}),
powershell: define<HasOutputContains>({
scriptOutputContains: (script, substring) => {
const inner: string = `${script} | Select-String ${substring}`
return `$($__out__ = $(${inner}); if ($__out__ -eq $null) { exit 1 } else { $__out__ })`
},
}),
}

16
e2e/shellcode/shells/redirect-output.ts

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
import { ScriptLine, define } from "./types.js"
type RedirectOutputOpts = { output: string }
export type HasRedirectOutput = {
redirectOutput(childCommand: ScriptLine, opts: RedirectOutputOpts): string
}
export const redirectOutput = {
bash: define<HasRedirectOutput>({
redirectOutput: (childCommand, opts) => `${childCommand} > ${opts.output}`,
}),
powershell: define<HasRedirectOutput>({
redirectOutput: (childCommand, opts) =>
`${childCommand} | Out-File ${opts.output} -Encoding UTF8`,
}),
}

20
e2e/shellcode/shells/sub-shell.ts

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
import { ScriptLine, define } from "./types.js"
import quote from "shell-escape"
type HasInSubShell = { inSubShell: (script: ScriptLine) => ScriptLine }
export const cmdInSubShell = {
bash: define<HasInSubShell>({
inSubShell: (script) => `echo ${quote([script])} | bash`,
}),
zsh: define<HasInSubShell>({
inSubShell: (script) => `echo ${quote([script])} | zsh`,
}),
fish: define<HasInSubShell>({
inSubShell: (script) => `fish -c ${quote([script])}`,
}),
powershell: define<HasInSubShell>({
inSubShell: (script) =>
`echo '${script.replace(/'/g, "\\'")}' | pwsh -NoProfile`,
}),
}

15
e2e/shellcode/shells/types.ts

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
export type Shell = {
escapeText(str: string): string
binaryName(): string
currentlySupported(): boolean
name(): string
launchArgs(): string[]
dieOnErrors?(): string
forceFile?: true | string
}
export type ScriptLine = string
export function define<S>(s: S): S {
return s
}

9
e2e/shellcode/test-bin-dir.ts

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
import { mkdirSync } from "node:fs"
import path from "node:path"
import testTmpDir from "./test-tmp-dir.js"
export default function testBinDir() {
const dir = path.join(testTmpDir(), "bin")
mkdirSync(dir, { recursive: true })
return dir
}

9
e2e/shellcode/test-cwd.ts

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
import { mkdirSync } from "node:fs"
import path from "node:path"
import testTmpDir from "./test-tmp-dir.js"
export default function testCwd() {
const dir = path.join(testTmpDir(), "cwd")
mkdirSync(dir, { recursive: true })
return dir
}

10
e2e/shellcode/test-node-version.ts

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
import { HasCall } from "./shells/cmdCall.js"
import { ScriptLine } from "./shells/types.js"
import { HasExpectCommandOutput } from "./shells/expect-command-output.js"
export default function testNodeVersion<
S extends HasCall & HasExpectCommandOutput
>(shell: S, version: string): ScriptLine {
const nodeVersion = shell.call("node", ["--version"])
return shell.hasCommandOutput(nodeVersion, version, "node version")
}

15
e2e/shellcode/test-tmp-dir.ts

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
import { mkdirSync, rmSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
export default function testTmpDir(): string {
const testName = (expect.getState().currentTestName ?? "unknown")
.toLowerCase()
.replace(/[^a-z0-9]/gi, "_")
.replace(/_+/g, "_")
const tmpDir = join(tmpdir(), `shellcode/${testName}`)
mkdirSync(tmpDir, { recursive: true })
rmSync(join(tmpDir, "fnm/aliases"), { recursive: true, force: true })
return tmpDir
}

49
e2e/shims.test.ts

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
import { readFile } from "node:fs/promises"
import { join } from "node:path"
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import testCwd from "./shellcode/test-cwd.js"
import describe from "./describe.js"
import testNodeVersion from "./shellcode/test-node-version.js"
for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
describe(shell, () => {
test(`outputs json`, async () => {
const filename = `file.json`
await script(shell)
.then(
shell.redirectOutput(
shell.call("fnm", ["env", "--json", "--with-shims"]),
{
output: filename,
}
)
)
.takeSnapshot(shell)
.execute(shell)
if (shell.currentlySupported()) {
const file = await readFile(join(testCwd(), filename), "utf8")
expect(JSON.parse(file)).toEqual({
FNM_ARCH: expect.any(String),
FNM_DIR: expect.any(String),
FNM_LOGLEVEL: "info",
FNM_MULTISHELL_PATH: expect.any(String),
FNM_NODE_DIST_MIRROR: expect.any(String),
FNM_VERSION_FILE_STRATEGY: "local",
FNM_VERSION_SWITCH_STRATEGY: "shims",
})
}
})
test(`runs Node through a shim`, async () => {
await script(shell)
.then(shell.env({ args: ["--with-shims"] }))
.then(shell.call("fnm", ["install", "12.0.0"]))
.then(shell.call("fnm", ["use", "12.0.0"]))
.then(testNodeVersion(shell, "v12.0.0"))
.takeSnapshot(shell)
.execute(shell)
})
})
}

38
e2e/system-node.test.ts

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js"
import fs from "node:fs/promises"
import path from "node:path"
import describe from "./describe.js"
import testNodeVersion from "./shellcode/test-node-version.js"
import testBinDir from "./shellcode/test-bin-dir.js"
for (const shell of [Bash, Fish, PowerShell, WinCmd, Zsh]) {
describe(shell, () => {
// latest bash breaks this as it seems. gotta find a solution.
const t = process.platform === "darwin" && shell === Bash ? test.skip : test
t(`switches to system node`, async () => {
const customNode = path.join(testBinDir(), "node")
if (
process.platform === "win32" &&
[WinCmd, PowerShell].includes(shell)
) {
await fs.writeFile(customNode + ".cmd", "@echo custom")
} else {
await fs.writeFile(customNode, `#!/bin/bash\n\necho "custom"\n`)
// set executable
await fs.chmod(customNode, 0o766)
}
await script(shell)
.then(shell.env({}))
.then(shell.call("fnm", ["install", "v10.10.0"]))
.then(shell.call("fnm", ["use", "v10"]))
.then(testNodeVersion(shell, "v10.10.0"))
.then(shell.call("fnm", ["use", "system"]))
.then(testNodeVersion(shell, "custom"))
.execute(shell)
})
})
}

29
e2e/uninstall.test.ts

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, Zsh } from "./shellcode/shells.js"
import describe from "./describe.js"
for (const shell of [Bash, Zsh, Fish, PowerShell]) {
describe(shell, () => {
test(`uninstalls a version`, async () => {
await script(shell)
.then(shell.call("fnm", ["install", "12.0.0"]))
.then(shell.call("fnm", ["alias", "12.0.0", "hello"]))
.then(
shell.scriptOutputContains(
shell.scriptOutputContains(shell.call("fnm", ["ls"]), "v12.0.0"),
"hello"
)
)
.then(shell.call("fnm", ["uninstall", "hello"]))
.then(
shell.hasCommandOutput(
shell.call("fnm", ["ls"]),
"* system",
"fnm ls"
)
)
.takeSnapshot(shell)
.execute(shell)
})
})
}

22
jest.config.cjs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest/presets/default-esm",
testEnvironment: "node",
testTimeout: 120000,
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
"#ansi-styles": "ansi-styles/index.js",
"#supports-color": "supports-color/index.js",
},
transform: {
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
},
],
},
}

33
package.json

@ -1,13 +1,15 @@ @@ -1,13 +1,15 @@
{
"name": "fnm",
"version": "0.0.0",
"version": "1.31.1",
"private": true,
"repository": "git@github.com:Schniz/fnm.git",
"author": "Gal Schlezinger <gal@spitfire.co.il>",
"type": "module",
"packageManager": "pnpm@7.16.1",
"license": "GPLv3",
"scripts": {
"changelog": "./.ci/generate-changelog.sh",
"version:prepare": "./.ci/prepare-version.js",
"test": "cross-env NODE_OPTIONS='--experimental-vm-modules' jest",
"version:prepare": "changeset version && ./.ci/prepare-version.js",
"generate-command-docs": "./.ci/print-command-docs.js"
},
"changelog": {
@ -20,10 +22,27 @@ @@ -20,10 +22,27 @@
}
},
"devDependencies": {
"cmd-ts": "0.8.0",
"execa": "5.1.1",
"@changesets/cli": "2.25.0",
"@svitejs/changesets-changelog-github-compact": "0.1.1",
"@types/jest": "^29.2.3",
"@types/node": "^18.11.9",
"@types/shell-escape": "^0.2.1",
"chalk": "^5.1.2",
"cmd-ts": "0.11.0",
"cross-env": "^7.0.3",
"execa": "6.1.0",
"jest": "^29.3.1",
"lerna-changelog": "2.2.0",
"prettier": "2.5.1",
"toml": "3.0.0"
"prettier": "2.7.1",
"pv": "1.0.1",
"shell-escape": "^0.2.0",
"svg-term-cli": "2.1.1",
"toml": "3.0.0",
"ts-dedent": "^2.2.0",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4"
},
"prettier": {
"semi": false
}
}

5129
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

28
renovate.json

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"labels": ["PR: Dependency Update"],
"packageRules": [
{
@ -7,9 +9,29 @@ @@ -7,9 +9,29 @@
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch"
}
},
{
"matchPackagePatterns": ["serde", "serde_json"],
"groupName": "serde",
"groupSlug": "serde"
},
{
"matchPackagePatterns": ["clap", "clap_*"],
"groupName": "clap-rs",
"groupSlug": "clap-rs"
},
{
"matchDepTypes": ["devDependencies", "dev-dependencies"],
"matchPackagePatterns": ["*"],
"automerge": true,
"groupName": "all dev dependencies",
"groupSlug": "all-dev-dependencies"
},
{
"matchPackagePatterns": ["uraimo/run-on-arch-action"],
"matchManagers": ["github-actions"],
"allowedVersions": "2.1.2"
}
],
"extends": [
"config:base"
]
"lockFileMaintenance": { "enabled": true }
}

5
site/package.json

@ -6,10 +6,5 @@ @@ -6,10 +6,5 @@
"license": "MIT",
"scripts": {
"build": "rm -rf public; mkdir public; cp ../.ci/install.sh ./public/install.txt"
},
"devDependencies": {
"@vercel/node": "1.12.1",
"typescript": "4.5.4",
"vercel": "23.1.2"
}
}

10
site/tsconfig.json

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

14
site/vercel.json

@ -1,17 +1,19 @@ @@ -1,17 +1,19 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"github": {
"silent": true
},
"redirects": [
{ "source": "/", "destination": "https://github.com/Schniz/fnm" }
],
"rewrites": [
{ "source": "/install", "destination": "/install.txt" }
],
"rewrites": [{ "source": "/install", "destination": "/install.txt" }],
"headers": [
{
"source": "/install",
"headers" : [
"headers": [
{
"key" : "Cache-Control",
"value" : "public, max-age=3600"
"key": "Cache-Control",
"value": "public, max-age=3600"
}
]
}

677
site/yarn.lock

@ -1,677 +0,0 @@ @@ -1,677 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
"@szmarczak/http-timer@^1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
dependencies:
defer-to-connect "^1.0.1"
"@types/color-name@^1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/node@*":
version "14.11.2"
resolved "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==
"@vercel/build-utils@2.12.2":
version "2.12.2"
resolved "https://registry.yarnpkg.com/@vercel/build-utils/-/build-utils-2.12.2.tgz#285a3bb0b78864fb6f44478257bd275c57aa8651"
integrity sha512-KbSgG2ZCVXhUsdbnpv6gC7buygd31jaKiKhrd4Lzv1NwjnoeDZAXlm4hzvSPYHVtCY2jirKJWP2rFtMW8iAh9g==
"@vercel/go@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@vercel/go/-/go-1.2.3.tgz#6f2bdba5681f9d64ee17060c5d63589e2d45e2d8"
integrity sha512-BZCHRz43Qfr0DwZlZQCcofR+3cr+H+HK72/ZPkZy1Uq0NYjJMlmZ3ahuMgvJxT9lfC1RA6eOEUlUsZ+gqKcMCg==
"@vercel/node@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@vercel/node/-/node-1.12.1.tgz#15f42f64690f904f8a52a387123ce0958657060f"
integrity sha512-NcawIY05BvVkWlsowaxF2hl/hJg475U8JvT2FnGykFPMx31q1/FtqyTw/awSrKfOSRXR0InrbEIDIelmS9NzPA==
dependencies:
"@types/node" "*"
ts-node "8.9.1"
typescript "4.3.4"
"@vercel/python@2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@vercel/python/-/python-2.0.5.tgz#76c09280febfac863c39651edffafbb0838a1df8"
integrity sha512-WCSTTw6He2COaSBiGDk2q5Q1ue+z5usRZcvUHCpsK6KvNkkV/PrY8JT73XQysMWKiXh6yQy19IUFAOqK/xwhig==
"@vercel/ruby@1.2.7":
version "1.2.7"
resolved "https://registry.yarnpkg.com/@vercel/ruby/-/ruby-1.2.7.tgz#516d1c45f4961619338f3d3bb518ee43b863a5da"
integrity sha512-ZG2VxMHHSKocL57UWsfNc9UsblwYGm55/ujqGIBnkNUURnRgtUrwtWlEts1eJ4VHD754Lc/0/R1pfJXoN5SbRw==
ansi-align@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb"
integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==
dependencies:
string-width "^3.0.0"
ansi-regex@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
ansi-regex@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
ansi-styles@^4.1.0:
version "4.2.1"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
dependencies:
"@types/color-name" "^1.1.1"
color-convert "^2.0.1"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
boxen@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64"
integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==
dependencies:
ansi-align "^3.0.0"
camelcase "^5.3.1"
chalk "^3.0.0"
cli-boxes "^2.2.0"
string-width "^4.1.0"
term-size "^2.1.0"
type-fest "^0.8.1"
widest-line "^3.1.0"
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
cacheable-request@^6.0.0:
version "6.1.0"
resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
dependencies:
clone-response "^1.0.2"
get-stream "^5.1.0"
http-cache-semantics "^4.0.0"
keyv "^3.0.0"
lowercase-keys "^2.0.0"
normalize-url "^4.1.0"
responselike "^1.0.2"
camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
cli-boxes@^2.2.0:
version "2.2.1"
resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
clone-response@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
dependencies:
mimic-response "^1.0.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
configstore@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==
dependencies:
dot-prop "^5.2.0"
graceful-fs "^4.1.2"
make-dir "^3.0.0"
unique-string "^2.0.0"
write-file-atomic "^3.0.0"
xdg-basedir "^4.0.0"
crypto-random-string@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
decompress-response@^3.3.0:
version "3.3.0"
resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
dependencies:
mimic-response "^1.0.0"
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
defer-to-connect@^1.0.1:
version "1.1.3"
resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
dot-prop@^5.2.0:
version "5.3.0"
resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==
dependencies:
is-obj "^2.0.0"
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
emoji-regex@^7.0.1:
version "7.0.3"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
escape-goat@^2.0.0:
version "2.1.1"
resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
get-stream@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
dependencies:
pump "^3.0.0"
get-stream@^5.1.0:
version "5.2.0"
resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
dependencies:
pump "^3.0.0"
global-dirs@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201"
integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==
dependencies:
ini "^1.3.5"
got@^9.6.0:
version "9.6.0"
resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
dependencies:
"@sindresorhus/is" "^0.14.0"
"@szmarczak/http-timer" "^1.1.2"
cacheable-request "^6.0.0"
decompress-response "^3.3.0"
duplexer3 "^0.1.4"
get-stream "^4.1.0"
lowercase-keys "^1.0.1"
mimic-response "^1.0.1"
p-cancelable "^1.0.0"
to-readable-stream "^1.0.0"
url-parse-lax "^3.0.0"
graceful-fs@^4.1.2:
version "4.2.4"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-yarn@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77"
integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
import-lazy@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
ini@^1.3.5, ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
dependencies:
ci-info "^2.0.0"
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-installed-globally@^0.3.1:
version "0.3.2"
resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141"
integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==
dependencies:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
is-npm@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==
is-obj@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
is-path-inside@^3.0.1:
version "3.0.2"
resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017"
integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==
is-typedarray@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-yarn-global@^0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
json-buffer@3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
keyv@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
dependencies:
json-buffer "3.0.0"
latest-version@^5.0.0:
version "5.1.0"
resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face"
integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==
dependencies:
package-json "^6.3.0"
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
lowercase-keys@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
make-dir@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
dependencies:
semver "^6.0.0"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
mimic-response@^1.0.0, mimic-response@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
minimist@^1.2.0:
version "1.2.5"
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
normalize-url@^4.1.0:
version "4.5.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
p-cancelable@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
package-json@^6.3.0:
version "6.5.0"
resolved "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==
dependencies:
got "^9.6.0"
registry-auth-token "^4.0.0"
registry-url "^5.0.0"
semver "^6.2.0"
prepend-http@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
pupa@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726"
integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==
dependencies:
escape-goat "^2.0.0"
rc@^1.2.8:
version "1.2.8"
resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
dependencies:
deep-extend "^0.6.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
registry-auth-token@^4.0.0:
version "4.2.0"
resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da"
integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==
dependencies:
rc "^1.2.8"
registry-url@^5.0.0:
version "5.1.0"
resolved "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009"
integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==
dependencies:
rc "^1.2.8"
responselike@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
dependencies:
lowercase-keys "^1.0.0"
semver-diff@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==
dependencies:
semver "^6.3.0"
semver@^6.0.0, semver@^6.2.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
source-map-support@^0.5.17:
version "0.5.19"
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
string-width@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
dependencies:
emoji-regex "^7.0.1"
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string-width@^4.0.0, string-width@^4.1.0:
version "4.2.0"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
strip-ansi@^5.1.0:
version "5.2.0"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
dependencies:
ansi-regex "^5.0.0"
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
term-size@^2.1.0:
version "2.2.0"
resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753"
integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==
to-readable-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
ts-node@8.9.1:
version "8.9.1"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.9.1.tgz#2f857f46c47e91dcd28a14e052482eb14cfd65a5"
integrity sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.17"
yn "3.1.1"
type-fest@^0.8.1:
version "0.8.1"
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
dependencies:
is-typedarray "^1.0.0"
typescript@4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc"
integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==
typescript@4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
unique-string@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==
dependencies:
crypto-random-string "^2.0.0"
update-notifier@4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3"
integrity sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==
dependencies:
boxen "^4.2.0"
chalk "^3.0.0"
configstore "^5.0.1"
has-yarn "^2.1.0"
import-lazy "^2.1.0"
is-ci "^2.0.0"
is-installed-globally "^0.3.1"
is-npm "^4.0.0"
is-yarn-global "^0.3.0"
latest-version "^5.0.0"
pupa "^2.0.1"
semver-diff "^3.1.1"
xdg-basedir "^4.0.0"
url-parse-lax@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
dependencies:
prepend-http "^2.0.0"
vercel@23.1.2:
version "23.1.2"
resolved "https://registry.yarnpkg.com/vercel/-/vercel-23.1.2.tgz#7f36772970c7c56f10de89983f03b3c0c72d294e"
integrity sha512-uS1k7wuXI6hbxiW+kn9vdAWL0bBi4jjVxc7Jwp8NhJjcRuzlydtt3gUEnhnC9AOIKQ4LxoAgmg50lSyYkrC8Hg==
dependencies:
"@vercel/build-utils" "2.12.2"
"@vercel/go" "1.2.3"
"@vercel/node" "1.12.1"
"@vercel/python" "2.0.5"
"@vercel/ruby" "1.2.7"
update-notifier "4.1.0"
widest-line@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
dependencies:
string-width "^4.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
write-file-atomic@^3.0.0:
version "3.0.3"
resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
dependencies:
imurmurhash "^0.1.4"
is-typedarray "^1.0.0"
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
xdg-basedir@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==

26
src/arch.rs

@ -11,6 +11,20 @@ pub enum Arch { @@ -11,6 +11,20 @@ pub enum Arch {
S390x,
}
impl Arch {
pub fn as_str(&self) -> &'static str {
match self {
Arch::X86 => "x86",
Arch::X64 => "x64",
Arch::Arm64 => "arm64",
Arch::Armv7l => "armv7l",
Arch::Ppc64le => "ppc64le",
Arch::Ppc64 => "ppc64",
Arch::S390x => "s390x",
}
}
}
#[cfg(unix)]
/// handle common case: Apple Silicon / Node < 16
pub fn get_safe_arch<'a>(arch: &'a Arch, version: &Version) -> &'a Arch {
@ -55,17 +69,7 @@ impl std::str::FromStr for Arch { @@ -55,17 +69,7 @@ impl std::str::FromStr for Arch {
impl std::fmt::Display for Arch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let arch_str = match self {
Arch::X86 => String::from("x86"),
Arch::X64 => String::from("x64"),
Arch::Arm64 => String::from("arm64"),
Arch::Armv7l => String::from("armv7l"),
Arch::Ppc64le => String::from("ppc64le"),
Arch::Ppc64 => String::from("ppc64"),
Arch::S390x => String::from("s390x"),
};
write!(f, "{}", arch_str)
write!(f, "{}", self.as_str())
}
}

4
src/archive/zip.rs

@ -42,7 +42,7 @@ impl<R: Read> Extract for Zip<R> { @@ -42,7 +42,7 @@ impl<R: Read> Extract for Zip<R> {
}
}
if (&*file.name()).ends_with('/') {
if file.name().ends_with('/') {
debug!(
"File {} extracted to \"{}\"",
i,
@ -58,7 +58,7 @@ impl<R: Read> Extract for Zip<R> { @@ -58,7 +58,7 @@ impl<R: Read> Extract for Zip<R> {
);
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(&p)?;
fs::create_dir_all(p)?;
}
}
let mut outfile = fs::File::create(&outpath)?;

24
src/choose_version_for_user_input.rs

@ -6,8 +6,8 @@ use crate::user_version::UserVersion; @@ -6,8 +6,8 @@ use crate::user_version::UserVersion;
use crate::version::Version;
use colored::Colorize;
use log::info;
use snafu::{ensure, ResultExt, Snafu};
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug)]
pub struct ApplicableVersion {
@ -29,8 +29,8 @@ pub fn choose_version_for_user_input<'a>( @@ -29,8 +29,8 @@ pub fn choose_version_for_user_input<'a>(
requested_version: &'a UserVersion,
config: &'a FnmConfig,
) -> Result<Option<ApplicableVersion>, Error> {
let all_versions =
installed_versions::list(config.installations_dir()).context(VersionListing)?;
let all_versions = installed_versions::list(config.installations_dir())
.map_err(|source| Error::VersionListing { source })?;
let result = if let UserVersion::Full(Version::Bypassed) = requested_version {
info!(
@ -54,18 +54,16 @@ pub fn choose_version_for_user_input<'a>( @@ -54,18 +54,16 @@ pub fn choose_version_for_user_input<'a>(
path: alias_path,
version: Version::Bypassed,
})
} else {
ensure!(
alias_path.exists(),
CantFindVersion {
requested_version: requested_version.clone()
}
);
} else if alias_path.exists() {
info!("Using Node for alias {}", alias_name.cyan());
Some(ApplicableVersion {
path: alias_path,
version: Version::Alias(alias_name),
})
} else {
return Err(Error::CantFindVersion {
requested_version: requested_version.clone(),
});
}
} else {
let current_version = requested_version.to_version(&all_versions, config);
@ -86,10 +84,10 @@ pub fn choose_version_for_user_input<'a>( @@ -86,10 +84,10 @@ pub fn choose_version_for_user_input<'a>(
Ok(result)
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display("Can't find requested version: {}", requested_version))]
#[error("Can't find requested version: {}", requested_version)]
CantFindVersion { requested_version: UserVersion },
#[snafu(display("Can't list local installed versions: {}", source))]
#[error("Can't list local installed versions: {}", source)]
VersionListing { source: installed_versions::Error },
}

38
src/cli.rs

@ -1,24 +1,24 @@ @@ -1,24 +1,24 @@
use crate::commands;
use crate::commands::command::Command;
use crate::config::FnmConfig;
use structopt::StructOpt;
use clap::Parser;
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub enum SubCommand {
/// List all remote Node.js versions
#[structopt(name = "list-remote", visible_aliases = &["ls-remote"])]
#[clap(name = "list-remote", bin_name = "list-remote", visible_aliases = &["ls-remote"])]
LsRemote(commands::ls_remote::LsRemote),
/// List all locally installed Node.js versions
#[structopt(name = "list", visible_aliases = &["ls"])]
#[clap(name = "list", bin_name = "list", visible_aliases = &["ls"])]
LsLocal(commands::ls_local::LsLocal),
/// Install a new Node.js version
#[structopt(name = "install")]
#[clap(name = "install", bin_name = "install")]
Install(commands::install::Install),
/// Change Node.js version
#[structopt(name = "use")]
#[clap(name = "use", bin_name = "use")]
Use(commands::r#use::Use),
/// Print and set up required environment variables for fnm
@ -29,29 +29,29 @@ pub enum SubCommand { @@ -29,29 +29,29 @@ pub enum SubCommand {
/// Each shell has its own syntax of evaluating a dynamic expression.
/// For example, evaluating fnm on Bash and Zsh would look like `eval "$(fnm env)"`.
/// In Fish, evaluating would look like `fnm env | source`
#[structopt(name = "env")]
#[clap(name = "env", bin_name = "env")]
Env(commands::env::Env),
/// Print shell completions to stdout
#[structopt(name = "completions")]
#[clap(name = "completions", bin_name = "completions")]
Completions(commands::completions::Completions),
/// Alias a version to a common name
#[structopt(name = "alias")]
#[clap(name = "alias", bin_name = "alias")]
Alias(commands::alias::Alias),
/// Remove an alias definition
#[structopt(name = "unalias")]
#[clap(name = "unalias", bin_name = "unalias")]
Unalias(commands::unalias::Unalias),
/// Set a version as the default version
///
/// This is a shorthand for `fnm alias VERSION default`
#[structopt(name = "default")]
#[clap(name = "default", bin_name = "default")]
Default(commands::default::Default),
/// Print the current Node.js version
#[structopt(name = "current")]
#[clap(name = "current", bin_name = "current")]
Current(commands::current::Current),
/// Run a command within fnm context
@ -60,14 +60,14 @@ pub enum SubCommand { @@ -60,14 +60,14 @@ pub enum SubCommand {
/// --------
/// fnm exec --using=v12.0.0 node --version
/// => v12.0.0
#[structopt(name = "exec", verbatim_doc_comment)]
#[clap(name = "exec", bin_name = "exec", verbatim_doc_comment)]
Exec(commands::exec::Exec),
/// Uninstall a Node.js version
///
/// > Warning: when providing an alias, it will remove the Node version the alias
/// is pointing to, along with the other aliases that point to the same version.
#[structopt(name = "uninstall")]
#[clap(name = "uninstall", bin_name = "uninstall")]
Uninstall(commands::uninstall::Uninstall),
}
@ -91,15 +91,15 @@ impl SubCommand { @@ -91,15 +91,15 @@ impl SubCommand {
}
/// A fast and simple Node.js manager.
#[derive(StructOpt, Debug)]
#[structopt(name = "fnm")]
#[derive(clap::Parser, Debug)]
#[clap(name = "fnm", version = env!("CARGO_PKG_VERSION"), bin_name = "fnm")]
pub struct Cli {
#[structopt(flatten)]
#[clap(flatten)]
pub config: FnmConfig,
#[structopt(subcommand)]
#[clap(subcommand)]
pub subcmd: SubCommand,
}
pub fn parse() -> Cli {
Cli::from_args()
Cli::parse()
}

22
src/commands/alias.rs

@ -4,12 +4,10 @@ use crate::choose_version_for_user_input::{ @@ -4,12 +4,10 @@ use crate::choose_version_for_user_input::{
choose_version_for_user_input, Error as ApplicableVersionError,
};
use crate::config::FnmConfig;
use crate::installed_versions;
use crate::user_version::UserVersion;
use snafu::{OptionExt, ResultExt, Snafu};
use structopt::StructOpt;
use thiserror::Error;
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub struct Alias {
pub(crate) to_version: UserVersion,
pub(crate) name: String,
@ -20,26 +18,24 @@ impl Command for Alias { @@ -20,26 +18,24 @@ impl Command for Alias {
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let applicable_version = choose_version_for_user_input(&self.to_version, config)
.context(CantUnderstandVersion)?
.context(VersionNotFound {
.map_err(|source| Error::CantUnderstandVersion { source })?
.ok_or(Error::VersionNotFound {
version: self.to_version,
})?;
create_alias(config, &self.name, applicable_version.version())
.context(CantCreateSymlink)?;
.map_err(|source| Error::CantCreateSymlink { source })?;
Ok(())
}
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display("Can't create symlink for alias: {}", source))]
#[error("Can't create symlink for alias: {}", source)]
CantCreateSymlink { source: std::io::Error },
#[snafu(display("Can't list local installed versions: {}", source))]
VersionListingError { source: installed_versions::Error },
#[snafu(display("Version {} not found locally", version))]
#[error("Version {} not found locally", version)]
VersionNotFound { version: UserVersion },
#[snafu(display("{}", source))]
#[error(transparent)]
CantUnderstandVersion { source: ApplicableVersionError },
}

21
src/commands/completions.rs

@ -2,14 +2,14 @@ use super::command::Command; @@ -2,14 +2,14 @@ use super::command::Command;
use crate::cli::Cli;
use crate::config::FnmConfig;
use crate::shell::{infer_shell, AVAILABLE_SHELLS};
use snafu::{OptionExt, Snafu};
use structopt::clap::Shell;
use structopt::StructOpt;
use clap::{IntoApp, Parser};
use clap_complete::{Generator, Shell};
use thiserror::Error;
#[derive(StructOpt, Debug)]
#[derive(Parser, Debug)]
pub struct Completions {
/// The shell syntax to use. Infers when missing.
#[structopt(long, possible_values = &Shell::variants())]
#[clap(long)]
shell: Option<Shell>,
}
@ -21,21 +21,22 @@ impl Command for Completions { @@ -21,21 +21,22 @@ impl Command for Completions {
let shell = self
.shell
.or_else(|| infer_shell().map(Into::into))
.context(CantInferShell)?;
Cli::clap().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut stdio);
.ok_or(Error::CantInferShell)?;
let app = Cli::command();
shell.generate(&app, &mut stdio);
Ok(())
}
}
#[derive(Snafu, Debug)]
#[derive(Error, Debug)]
pub enum Error {
#[snafu(display(
#[error(
"{}\n{}\n{}\n{}",
"Can't infer shell!",
"fnm can't infer your shell based on the process tree.",
"Maybe it is unsupported? we support the following shells:",
shells_as_string()
))]
)]
CantInferShell,
}

3
src/commands/current.rs

@ -1,9 +1,8 @@ @@ -1,9 +1,8 @@
use super::command::Command;
use crate::config::FnmConfig;
use crate::current_version::{current_version, Error};
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub struct Current {}
impl Command for Current {

3
src/commands/default.rs

@ -2,9 +2,8 @@ use super::alias::Alias; @@ -2,9 +2,8 @@ use super::alias::Alias;
use super::command::Command;
use crate::config::FnmConfig;
use crate::user_version::UserVersion;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub struct Default {
version: UserVersion,
}

124
src/commands/env.rs

@ -1,25 +1,30 @@ @@ -1,25 +1,30 @@
use super::command::Command;
use crate::config::FnmConfig;
use crate::directories;
use crate::fs::symlink_dir;
use crate::path_ext::PathExt;
use crate::shell::{infer_shell, Shell, AVAILABLE_SHELLS};
use crate::version_switch_strategy::VersionSwitchStrategy;
use crate::{outln, shims};
use colored::Colorize;
use snafu::{OptionExt, ResultExt, Snafu};
use std::collections::HashMap;
use std::fmt::Debug;
use structopt::StructOpt;
use thiserror::Error;
#[derive(StructOpt, Debug, Default)]
#[derive(clap::Parser, Debug, Default)]
pub struct Env {
/// The shell syntax to use. Infers when missing.
#[structopt(long)]
#[structopt(possible_values = AVAILABLE_SHELLS)]
#[clap(long)]
#[clap(possible_values = AVAILABLE_SHELLS)]
shell: Option<Box<dyn Shell>>,
/// Print JSON instead of shell commands.
#[clap(long, conflicts_with = "shell")]
json: bool,
/// Deprecated. This is the default now.
#[structopt(long, hidden = true)]
#[clap(long, hide = true)]
multi: bool,
/// Print the script to change Node versions every directory change
#[structopt(long)]
#[clap(long)]
use_on_cd: bool,
/// Adds `node` shims to your PATH environment variable
@ -37,18 +42,18 @@ fn generate_symlink_path() -> String { @@ -37,18 +42,18 @@ fn generate_symlink_path() -> String {
)
}
fn make_symlink(config: &FnmConfig) -> std::path::PathBuf {
let base_dir = std::env::temp_dir()
.join("fnm_multishells")
.ensure_exists_silently();
fn make_symlink(config: &FnmConfig) -> Result<std::path::PathBuf, Error> {
let base_dir = directories::multishell_storage().ensure_exists_silently();
let mut temp_dir = base_dir.join(generate_symlink_path());
while temp_dir.exists() {
temp_dir = base_dir.join(generate_symlink_path());
}
symlink_dir(config.default_version_dir(), &temp_dir).expect("Can't create symlink!");
temp_dir
match symlink_dir(config.default_version_dir(), &temp_dir) {
Ok(_) => Ok(temp_dir),
Err(source) => Err(Error::CantCreateSymlink { source, temp_dir }),
}
}
impl Command for Env {
@ -65,58 +70,91 @@ impl Command for Env { @@ -65,58 +70,91 @@ impl Command for Env {
);
}
let shell: Box<dyn Shell> = self.shell.or_else(&infer_shell).context(CantInferShell)?;
let multishell_path = make_symlink(config);
let multishell_path = make_symlink(config)?;
let binary_path = if self.with_shims {
shims::store_shim(config).context(CantCreateShims)?
shims::store_shim(config).map_err(|source| Error::CantCreateShims { source })?
} else if cfg!(windows) {
multishell_path.clone()
} else {
multishell_path.join("bin")
};
println!("{}", shell.path(&binary_path));
println!(
"{}",
shell.set_env_var("FNM_MULTISHELL_PATH", multishell_path.to_str().unwrap())
);
println!(
"{}",
shell.set_env_var("FNM_DIR", config.base_dir_with_default().to_str().unwrap())
);
println!(
"{}",
shell.set_env_var("FNM_LOGLEVEL", config.log_level().clone().into())
);
println!(
"{}",
shell.set_env_var("FNM_NODE_DIST_MIRROR", config.node_dist_mirror.as_str())
);
println!(
"{}",
shell.set_env_var("FNM_ARCH", &config.arch.to_string())
);
let fnm_dir = config.base_dir_with_default();
let env_vars = HashMap::from([
("FNM_MULTISHELL_PATH", multishell_path.to_str().unwrap()),
(
"FNM_VERSION_FILE_STRATEGY",
config.version_file_strategy().as_str(),
),
("FNM_DIR", fnm_dir.to_str().unwrap()),
("FNM_LOGLEVEL", config.log_level().as_str()),
("FNM_NODE_DIST_MIRROR", config.node_dist_mirror.as_str()),
("FNM_ARCH", config.arch.as_str()),
(
"FNM_VERSION_SWITCH_STRATEGY",
if self.with_shims {
VersionSwitchStrategy::Shims
} else {
VersionSwitchStrategy::PathSymlink
}
.as_str(),
),
]);
if self.json {
println!("{}", serde_json::to_string(&env_vars).unwrap());
return Ok(());
}
let shell: Box<dyn Shell> = self
.shell
.or_else(infer_shell)
.ok_or(Error::CantInferShell)?;
println!("{}", shell.path(&binary_path)?);
for (name, value) in &env_vars {
println!("{}", shell.set_env_var(name, value));
}
if self.use_on_cd {
println!("{}", shell.use_on_cd(config));
println!("{}", shell.use_on_cd(config)?);
}
if let Some(v) = shell.rehash() {
println!("{}", v);
}
Ok(())
}
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display(
#[error(
"{}\n{}\n{}\n{}",
"Can't infer shell!",
"fnm can't infer your shell based on the process tree.",
"Maybe it is unsupported? we support the following shells:",
shells_as_string()
))]
)]
CantInferShell,
#[snafu(display("Can't create Node.js shims: {}", source))]
CantCreateShims { source: std::io::Error },
#[error("Can't create Node.js shims: {}", source)]
CantCreateShims {
#[source]
source: std::io::Error,
},
#[error("Can't create the symlink for multishells at {temp_dir:?}. Maybe there are some issues with permissions for the directory? {source}")]
CantCreateSymlink {
#[source]
source: std::io::Error,
temp_dir: std::path::PathBuf,
},
#[error(transparent)]
ShellError {
#[from]
source: anyhow::Error,
},
}
fn shells_as_string() -> String {

72
src/commands/exec.rs

@ -9,20 +9,19 @@ use crate::user_version::UserVersion; @@ -9,20 +9,19 @@ use crate::user_version::UserVersion;
use crate::user_version_reader::UserVersionReader;
use colored::Colorize;
use log::debug;
use snafu::{OptionExt, ResultExt, Snafu};
use std::process::{Command, Stdio};
use structopt::StructOpt;
use thiserror::Error;
#[derive(Debug, StructOpt)]
#[structopt(setting = structopt::clap::AppSettings::TrailingVarArg)]
#[derive(Debug, clap::Parser)]
#[clap(trailing_var_arg = true)]
pub struct Exec {
/// Either an explicit version, or a filename with the version written in it
#[structopt(long = "using")]
#[clap(long = "using")]
version: Option<UserVersionReader>,
/// Deprecated. This is the default now.
#[structopt(long = "using-file", hidden = true)]
#[clap(long = "using-file", hide = true)]
using_file: bool,
#[structopt(long = "using-current", hidden = true, conflicts_with = "using")]
#[structopt(long = "using-current", hidden = true, conflicts_with = "version")]
using_current: bool,
/// The command to run
arguments: Vec<String>,
@ -42,12 +41,15 @@ impl Cmd for Exec { @@ -42,12 +41,15 @@ impl Cmd for Exec {
);
}
let (binary, arguments) = self.arguments.split_first().context(NoBinaryProvided)?;
let (binary, arguments) = self
.arguments
.split_first()
.ok_or(Error::NoBinaryProvided)?;
let version = if self.using_current {
let version = current_version(config)
.context(CantGetCurrentVersion)?
.context(NoCurrentVersion)?;
.map_err(|source| Error::CantGetCurrentVersion { source })?
.ok_or(Error::NoCurrentVersion)?;
UserVersion::Full(version)
} else {
self.version
@ -56,13 +58,13 @@ impl Cmd for Exec { @@ -56,13 +58,13 @@ impl Cmd for Exec {
let current_dir = std::env::current_dir().unwrap();
UserVersionReader::Path(current_dir)
})
.into_user_version()
.context(CantInferVersion)?
.into_user_version(config)
.ok_or(Error::CantInferVersion)?
};
let applicable_version = choose_version_for_user_input(&version, config)
.context(ApplicableVersionError)?
.context(VersionNotFound { version })?;
.map_err(|source| Error::ApplicableVersionError { source })?
.ok_or(Error::VersionNotFound { version })?;
#[cfg(windows)]
let bin_path = applicable_version.path().to_path_buf();
@ -73,13 +75,14 @@ impl Cmd for Exec { @@ -73,13 +75,14 @@ impl Cmd for Exec {
debug!("Using Node.js from {}", bin_path.display());
let path_env = {
let paths_env = std::env::var_os("PATH").context(CantReadPathVariable)?;
let paths_env = std::env::var_os("PATH").ok_or(Error::CantReadPathVariable)?;
let mut paths: Vec<_> = std::env::split_paths(&paths_env).collect();
paths.insert(0, bin_path);
std::env::join_paths(paths).context(CantAddPathToEnvironment)?
std::env::join_paths(paths)
.map_err(|source| Error::CantAddPathToEnvironment { source })?
};
let exit_status = Command::new(&binary)
let exit_status = Command::new(binary)
.args(arguments)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
@ -90,40 +93,35 @@ impl Cmd for Exec { @@ -90,40 +93,35 @@ impl Cmd for Exec {
.wait()
.expect("Failed to grab exit code");
let code = exit_status.code().context(CantReadProcessExitCode)?;
let code = exit_status.code().ok_or(Error::CantReadProcessExitCode)?;
std::process::exit(code);
}
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display("Can't read path environment variable"))]
#[error("Can't read path environment variable")]
CantReadPathVariable,
#[snafu(display("Can't add path to environment variable: {}", source))]
CantAddPathToEnvironment {
source: std::env::JoinPathsError,
},
#[snafu(display(
"Can't find version in dotfiles. Please provide a version manually to the command."
))]
#[error("Can't add path to environment variable: {}", source)]
CantAddPathToEnvironment { source: std::env::JoinPathsError },
#[error("Can't find version in dotfiles. Please provide a version manually to the command.")]
CantInferVersion,
#[snafu(display("Requested version {} is not currently installed", version))]
VersionNotFound {
version: UserVersion,
},
#[error("Requested version {} is not currently installed", version)]
VersionNotFound { version: UserVersion },
#[error(transparent)]
ApplicableVersionError {
#[from]
source: UserInputError,
},
#[snafu(display(
"Can't read exit code from process.\nMaybe the process was killed using a signal?"
))]
#[error("Can't read exit code from process.\nMaybe the process was killed using a signal?")]
CantReadProcessExitCode,
#[snafu(display("{}", source))]
#[error("{}", source)]
CantGetCurrentVersion {
#[source]
source: current_version::Error,
},
#[snafu(display("No current version. Please run `fnm use <version>` and retry."))]
#[error("No current version. Please run `fnm use <version>` and retry.")]
NoCurrentVersion,
#[snafu(display("command not provided. Please provide a command to run as an argument, like {} or {}.\n{} {}", "node".italic(), "bash".italic(), "example:".yellow().bold(), "fnm exec --using=12 node --version".italic().yellow()))]
#[error("command not provided. Please provide a command to run as an argument, like {} or {}.\n{} {}", "node".italic(), "bash".italic(), "example:".yellow().bold(), "fnm exec --using=12 node --version".italic().yellow())]
NoBinaryProvided,
}

70
src/commands/install.rs

@ -10,16 +10,15 @@ use crate::version::Version; @@ -10,16 +10,15 @@ use crate::version::Version;
use crate::version_files::get_user_version_for_directory;
use colored::Colorize;
use log::debug;
use snafu::{ensure, OptionExt, ResultExt, Snafu};
use structopt::StructOpt;
use thiserror::Error;
#[derive(StructOpt, Debug, Default)]
#[derive(clap::Parser, Debug, Default)]
pub struct Install {
/// A version string. Can be a partial semver or a LTS version name by the format lts/NAME
pub version: Option<UserVersion>,
/// Install latest LTS
#[structopt(long, conflicts_with = "version")]
#[clap(long, conflicts_with = "version")]
pub lts: bool,
}
@ -50,21 +49,20 @@ impl super::command::Command for Install { @@ -50,21 +49,20 @@ impl super::command::Command for Install {
let current_version = self
.version()?
.or_else(|| get_user_version_for_directory(current_dir))
.context(CantInferVersion)?;
.or_else(|| get_user_version_for_directory(current_dir, config))
.ok_or(Error::CantInferVersion)?;
let version = match current_version.clone() {
UserVersion::Full(Version::Semver(actual_version)) => Version::Semver(actual_version),
UserVersion::Full(v @ (Version::Bypassed | Version::Alias(_))) => {
ensure!(false, UninstallableVersion { version: v });
unreachable!();
return Err(Error::UninstallableVersion { version: v });
}
UserVersion::Full(Version::Lts(lts_type)) => {
let available_versions: Vec<_> = remote_node_index::list(&config.node_dist_mirror)
.context(CantListRemoteVersions)?;
.map_err(|source| Error::CantListRemoteVersions { source })?;
let picked_version = lts_type
.pick_latest(&available_versions)
.with_context(|| CantFindRelevantLts {
.ok_or_else(|| Error::CantFindRelevantLts {
lts_type: lts_type.clone(),
})?
.version
@ -78,14 +76,14 @@ impl super::command::Command for Install { @@ -78,14 +76,14 @@ impl super::command::Command for Install {
}
current_version => {
let available_versions: Vec<_> = remote_node_index::list(&config.node_dist_mirror)
.context(CantListRemoteVersions)?
.map_err(|source| Error::CantListRemoteVersions { source })?
.drain(..)
.map(|x| x.version)
.collect();
current_version
.to_version(&available_versions, config)
.context(CantFindNodeVersion {
.ok_or(Error::CantFindNodeVersion {
requested_version: current_version,
})?
.clone()
@ -113,7 +111,7 @@ impl super::command::Command for Install { @@ -113,7 +111,7 @@ impl super::command::Command for Install {
Err(err @ DownloaderError::VersionAlreadyInstalled { .. }) => {
outln!(config, Error, "{} {}", "warning:".bold().yellow(), err);
}
other_err => other_err.context(DownloadError)?,
other_err => other_err.map_err(|source| Error::DownloadError { source })?,
};
if let UserVersion::Full(Version::Lts(lts_type)) = current_version {
@ -123,51 +121,41 @@ impl super::command::Command for Install { @@ -123,51 +121,41 @@ impl super::command::Command for Install {
alias_name.cyan(),
version.v_str().cyan()
);
create_alias(config, &alias_name, &version).context(IoError)?;
create_alias(config, &alias_name, &version)?;
}
if !config.default_version_dir().exists() {
debug!("Tagging {} as the default version", version.v_str().cyan());
create_alias(config, "default", &version).context(IoError)?;
create_alias(config, "default", &version)?;
}
Ok(())
}
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display("Can't download the requested binary: {}", source))]
DownloadError {
source: DownloaderError,
},
#[error("Can't download the requested binary: {}", source)]
DownloadError { source: DownloaderError },
#[error(transparent)]
IoError {
#[from]
source: std::io::Error,
},
#[snafu(display(
"Can't find version in dotfiles. Please provide a version manually to the command."
))]
#[error("Can't find version in dotfiles. Please provide a version manually to the command.")]
CantInferVersion,
#[snafu(display("Having a hard time listing the remote versions: {}", source))]
CantListRemoteVersions {
source: crate::http::Error,
},
#[snafu(display(
#[error("Having a hard time listing the remote versions: {}", source)]
CantListRemoteVersions { source: crate::http::Error },
#[error(
"Can't find a Node version that matches {} in remote",
requested_version
))]
CantFindNodeVersion {
requested_version: UserVersion,
},
#[snafu(display("Can't find relevant LTS named {}", lts_type))]
CantFindRelevantLts {
lts_type: crate::lts::LtsType,
},
#[snafu(display("The requested version is not installable: {}", version.v_str()))]
UninstallableVersion {
version: Version,
},
#[snafu(display("Too many versions provided. Please don't use --lts with a version string."))]
)]
CantFindNodeVersion { requested_version: UserVersion },
#[error("Can't find relevant LTS named {}", lts_type)]
CantFindRelevantLts { lts_type: crate::lts::LtsType },
#[error("The requested version is not installable: {}", version.v_str())]
UninstallableVersion { version: Version },
#[error("Too many versions provided. Please don't use --lts with a version string.")]
TooManyVersionsProvided,
}

20
src/commands/ls_local.rs

@ -3,11 +3,10 @@ use crate::config::FnmConfig; @@ -3,11 +3,10 @@ use crate::config::FnmConfig;
use crate::current_version::current_version;
use crate::version::Version;
use colored::Colorize;
use snafu::{ResultExt, Snafu};
use std::collections::HashMap;
use structopt::StructOpt;
use thiserror::Error;
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub struct LsLocal {}
impl super::command::Command for LsLocal {
@ -15,16 +14,17 @@ impl super::command::Command for LsLocal { @@ -15,16 +14,17 @@ impl super::command::Command for LsLocal {
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let base_dir = config.installations_dir();
let mut versions =
crate::installed_versions::list(base_dir).context(CantListLocallyInstalledVersion)?;
let mut versions = crate::installed_versions::list(base_dir)
.map_err(|source| Error::CantListLocallyInstalledVersion { source })?;
versions.insert(0, Version::Bypassed);
versions.sort();
let aliases_hash = generate_aliases_hash(config).context(CantReadAliases)?;
let aliases_hash =
generate_aliases_hash(config).map_err(|source| Error::CantReadAliases { source })?;
let curr_version = current_version(config).ok().flatten();
for version in versions {
let version_aliases = match aliases_hash.get(&version.v_str()) {
None => "".into(),
None => String::new(),
Some(versions) => {
let version_string = versions
.iter()
@ -60,12 +60,12 @@ fn generate_aliases_hash(config: &FnmConfig) -> std::io::Result<HashMap<String, @@ -60,12 +60,12 @@ fn generate_aliases_hash(config: &FnmConfig) -> std::io::Result<HashMap<String,
Ok(hashmap)
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display("Can't list locally installed versions: {}", source))]
#[error("Can't list locally installed versions: {}", source)]
CantListLocallyInstalledVersion {
source: crate::installed_versions::Error,
},
#[snafu(display("Can't read aliases: {}", source))]
#[error("Can't read aliases: {}", source)]
CantReadAliases { source: std::io::Error },
}

15
src/commands/ls_remote.rs

@ -1,16 +1,15 @@ @@ -1,16 +1,15 @@
use crate::config::FnmConfig;
use crate::remote_node_index;
use snafu::{ResultExt, Snafu};
use structopt::StructOpt;
use thiserror::Error;
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub struct LsRemote {}
impl super::command::Command for LsRemote {
type Error = Error;
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let all_versions = remote_node_index::list(&config.node_dist_mirror).context(HttpError)?;
let all_versions = remote_node_index::list(&config.node_dist_mirror)?;
for version in all_versions {
print!("{}", version.version);
@ -24,7 +23,11 @@ impl super::command::Command for LsRemote { @@ -24,7 +23,11 @@ impl super::command::Command for LsRemote {
}
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
HttpError { source: crate::http::Error },
#[error(transparent)]
HttpError {
#[from]
source: crate::http::Error,
},
}

16
src/commands/unalias.rs

@ -3,10 +3,9 @@ use crate::fs::remove_symlink_dir; @@ -3,10 +3,9 @@ use crate::fs::remove_symlink_dir;
use crate::user_version::UserVersion;
use crate::version::Version;
use crate::{choose_version_for_user_input, config::FnmConfig};
use snafu::{OptionExt, ResultExt, Snafu};
use structopt::StructOpt;
use thiserror::Error;
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub struct Unalias {
pub(crate) requested_alias: String,
}
@ -21,20 +20,21 @@ impl Command for Unalias { @@ -21,20 +20,21 @@ impl Command for Unalias {
)
.ok()
.flatten()
.with_context(|| AliasNotFound {
.ok_or(Error::AliasNotFound {
requested_alias: self.requested_alias,
})?;
remove_symlink_dir(&requested_version.path()).context(CantDeleteSymlink)?;
remove_symlink_dir(requested_version.path())
.map_err(|source| Error::CantDeleteSymlink { source })?;
Ok(())
}
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display("Can't delete symlink: {}", source))]
#[error("Can't delete symlink: {}", source)]
CantDeleteSymlink { source: std::io::Error },
#[snafu(display("Requested alias {} not found", requested_alias))]
#[error("Requested alias {} not found", requested_alias)]
AliasNotFound { requested_alias: String },
}

70
src/commands/uninstall.rs

@ -8,10 +8,9 @@ use crate::version::Version; @@ -8,10 +8,9 @@ use crate::version::Version;
use crate::version_files::get_user_version_for_directory;
use colored::Colorize;
use log::debug;
use snafu::{ensure, OptionExt, ResultExt, Snafu};
use structopt::StructOpt;
use thiserror::Error;
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub struct Uninstall {
version: Option<UserVersion>,
}
@ -20,49 +19,48 @@ impl Command for Uninstall { @@ -20,49 +19,48 @@ impl Command for Uninstall {
type Error = Error;
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let all_versions =
installed_versions::list(config.installations_dir()).context(VersionListingError)?;
let all_versions = installed_versions::list(config.installations_dir())
.map_err(|source| Error::VersionListingError { source })?;
let requested_version = self
.version
.or_else(|| {
let current_dir = std::env::current_dir().unwrap();
get_user_version_for_directory(current_dir)
get_user_version_for_directory(current_dir, config)
})
.context(CantInferVersion)?;
.ok_or(Error::CantInferVersion)?;
ensure!(
!matches!(requested_version, UserVersion::Full(Version::Bypassed)),
CantUninstallSystemVersion
);
if matches!(requested_version, UserVersion::Full(Version::Bypassed)) {
return Err(Error::CantUninstallSystemVersion);
}
let available_versions: Vec<&Version> = all_versions
.iter()
.filter(|v| requested_version.matches(v, config))
.collect();
ensure!(
available_versions.len() < 2,
PleaseBeMoreSpecificToDelete {
if available_versions.len() >= 2 {
return Err(Error::PleaseBeMoreSpecificToDelete {
matched_versions: available_versions
.iter()
.map(|x| x.v_str())
.collect::<Vec<_>>()
.map(std::string::ToString::to_string)
.collect(),
});
}
);
let version = requested_version
.to_version(&all_versions, config)
.context(CantFindVersion)?;
.ok_or(Error::CantFindVersion)?;
let matching_aliases = version.find_aliases(config).context(IoError)?;
let matching_aliases = version.find_aliases(config)?;
let root_path = version
.root_path(config)
.with_context(|| RootPathNotFound {
.ok_or_else(|| Error::RootPathNotFound {
version: version.clone(),
})?;
debug!("Removing Node version from {:?}", root_path);
std::fs::remove_dir_all(root_path).context(CantDeleteNodeVersion)?;
std::fs::remove_dir_all(root_path)
.map_err(|source| Error::CantDeleteNodeVersion { source })?;
outln!(
config,
Info,
@ -72,7 +70,8 @@ impl Command for Uninstall { @@ -72,7 +70,8 @@ impl Command for Uninstall {
for alias in matching_aliases {
debug!("Removing alias from {:?}", alias.path());
remove_symlink_dir(alias.path()).context(CantDeleteSymlink)?;
remove_symlink_dir(alias.path())
.map_err(|source| Error::CantDeleteSymlink { source })?;
outln!(
config,
Info,
@ -85,26 +84,27 @@ impl Command for Uninstall { @@ -85,26 +84,27 @@ impl Command for Uninstall {
}
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display("Can't get locally installed versions: {}", source))]
#[error("Can't get locally installed versions: {}", source)]
VersionListingError { source: installed_versions::Error },
#[snafu(display(
"Can't find version in dotfiles. Please provide a version manually to the command."
))]
#[error("Can't find version in dotfiles. Please provide a version manually to the command.")]
CantInferVersion,
#[snafu(display("Can't uninstall system version"))]
#[error("Can't uninstall system version")]
CantUninstallSystemVersion,
#[snafu(display("Too many versions had matched, please be more specific.\nFound {} matching versions, expected 1:\n{}", matched_versions.len(), matched_versions.iter().map(|v| format!("* {}", v)).collect::<Vec<_>>().join("\n")))]
#[error("Too many versions had matched, please be more specific.\nFound {} matching versions, expected 1:\n{}", matched_versions.len(), matched_versions.iter().map(|v| format!("* {}", v)).collect::<Vec<_>>().join("\n"))]
PleaseBeMoreSpecificToDelete { matched_versions: Vec<String> },
#[snafu(display("Can't find a matching version"))]
#[error("Can't find a matching version")]
CantFindVersion,
#[snafu(display("Root path not found for version {}", version))]
#[error("Root path not found for version {}", version)]
RootPathNotFound { version: Version },
#[snafu(display("io error: {}", source))]
IoError { source: std::io::Error },
#[snafu(display("Can't delete Node.js version: {}", source))]
#[error("io error: {}", source)]
IoError {
#[from]
source: std::io::Error,
},
#[error("Can't delete Node.js version: {}", source)]
CantDeleteNodeVersion { source: std::io::Error },
#[snafu(display("Can't delete symlink: {}", source))]
#[error("Can't delete symlink: {}", source)]
CantDeleteSymlink { source: std::io::Error },
}

138
src/commands/use.rs

@ -1,65 +1,75 @@ @@ -1,65 +1,75 @@
use super::command::Command;
use super::install::Install;
use crate::current_version::current_version;
use crate::fs;
use crate::installed_versions;
use crate::outln;
use crate::system_version;
use crate::user_version::UserVersion;
use crate::version::Version;
use crate::version_file_strategy::VersionFileStrategy;
use crate::version_switch_strategy::VersionSwitchStrategy;
use crate::{config::FnmConfig, user_version_reader::UserVersionReader};
use colored::Colorize;
use snafu::{ensure, OptionExt, ResultExt, Snafu};
use structopt::StructOpt;
use std::path::Path;
use thiserror::Error;
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub struct Use {
version: Option<UserVersionReader>,
/// Install the version if it isn't installed yet
#[structopt(long)]
#[clap(long)]
install_if_missing: bool,
/// Don't output a message identifying the version being used
/// if it will not change due to execution of this command
#[clap(long)]
silent_if_unchanged: bool,
}
impl Command for Use {
type Error = Error;
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let multishell_path = config.multishell_path().context(FnmEnvWasNotSourced)?;
let multishell_path = config.multishell_path().ok_or(Error::FnmEnvWasNotSourced)?;
warn_if_multishell_path_not_in_path_env_var(multishell_path, config);
let all_versions =
installed_versions::list(config.installations_dir()).context(VersionListingError)?;
let all_versions = installed_versions::list(config.installations_dir())
.map_err(|source| Error::VersionListingError { source })?;
let requested_version = self
.version
.unwrap_or_else(|| {
let current_dir = std::env::current_dir().unwrap();
UserVersionReader::Path(current_dir)
})
.into_user_version()
.context(CantInferVersion)?;
.into_user_version(config)
.ok_or_else(|| match config.version_file_strategy() {
VersionFileStrategy::Local => InferVersionError::Local,
VersionFileStrategy::Recursive => InferVersionError::Recursive,
})
.map_err(|source| Error::CantInferVersion { source })?;
let version_path = if let UserVersion::Full(Version::Bypassed) = requested_version {
outln!(
config,
Info,
let (message, version_path) = if let UserVersion::Full(Version::Bypassed) =
requested_version
{
let message = format!(
"Bypassing fnm: using {} node",
system_version::display_name().cyan()
);
system_version::path()
(message, system_version::path())
} else if let Some(alias_name) = requested_version.alias_name() {
let alias_path = config.aliases_dir().join(&alias_name);
let system_path = system_version::path();
if matches!(fs::shallow_read_symlink(&alias_path), Ok(shallow_path) if shallow_path == system_path)
{
outln!(
config,
Info,
let message = format!(
"Bypassing fnm: using {} node",
system_version::display_name().cyan()
);
system_path
(message, system_path)
} else if alias_path.exists() {
outln!(config, Info, "Using Node for alias {}", alias_name.cyan());
alias_path
let message = format!("Using Node for alias {}", alias_name.cyan());
(message, alias_path)
} else {
install_new_version(requested_version, config, self.install_if_missing)?;
return Ok(());
@ -67,45 +77,67 @@ impl Command for Use { @@ -67,45 +77,67 @@ impl Command for Use {
} else {
let current_version = requested_version.to_version(&all_versions, config);
if let Some(version) = current_version {
outln!(config, Info, "Using Node {}", version.to_string().cyan());
config
let version_path = config
.installations_dir()
.join(version.to_string())
.join("installation")
.join("installation");
let message = format!("Using Node {}", version.to_string().cyan());
(message, version_path)
} else {
install_new_version(requested_version, config, self.install_if_missing)?;
return Ok(());
}
};
replace_symlink(&version_path, multishell_path).context(SymlinkingCreationIssue)?;
if !self.silent_if_unchanged || will_version_change(&version_path, config) {
outln!(config, Info, "{}", message);
}
if let Some(multishells_path) = multishell_path.parent() {
std::fs::create_dir_all(multishells_path).map_err(|_err| {
Error::MultishellDirectoryCreationIssue {
path: multishells_path.to_path_buf(),
}
})?;
}
replace_symlink(&version_path, multishell_path)
.map_err(|source| Error::SymlinkingCreationIssue { source })?;
Ok(())
}
}
fn will_version_change(resolved_path: &Path, config: &FnmConfig) -> bool {
let current_version_path = current_version(config)
.unwrap_or(None)
.map(|v| v.installation_path(config));
current_version_path.as_deref() != Some(resolved_path)
}
fn install_new_version(
requested_version: UserVersion,
config: &FnmConfig,
install_if_missing: bool,
) -> Result<(), Error> {
ensure!(
install_if_missing || should_install_interactively(&requested_version),
CantFindVersion {
version: requested_version
if !install_if_missing && !should_install_interactively(&requested_version) {
return Err(Error::CantFindVersion {
version: requested_version,
});
}
);
Install {
version: Some(requested_version.clone()),
..Install::default()
}
.apply(config)
.context(InstallError)?;
.map_err(|source| Error::InstallError { source })?;
Use {
version: Some(UserVersionReader::Direct(requested_version)),
install_if_missing: true,
silent_if_unchanged: false,
}
.apply(config)?;
@ -119,8 +151,8 @@ fn install_new_version( @@ -119,8 +151,8 @@ fn install_new_version(
///
/// This way, we can create a symlink if it is missing.
fn replace_symlink(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> {
let symlink_deletion_result = fs::remove_symlink_dir(&to);
match fs::symlink_dir(&from, &to) {
let symlink_deletion_result = fs::remove_symlink_dir(to);
match fs::symlink_dir(from, to) {
ok @ Ok(_) => ok,
err @ Err(_) => symlink_deletion_result.and(err),
}
@ -153,6 +185,13 @@ fn warn_if_multishell_path_not_in_path_env_var( @@ -153,6 +185,13 @@ fn warn_if_multishell_path_not_in_path_env_var(
multishell_path: &std::path::Path,
config: &FnmConfig,
) {
if matches!(
config.version_switch_strategy(),
VersionSwitchStrategy::Shims
) {
return;
}
let bin_path = if cfg!(unix) {
multishell_path.join("bin")
} else {
@ -175,27 +214,36 @@ fn warn_if_multishell_path_not_in_path_env_var( @@ -175,27 +214,36 @@ fn warn_if_multishell_path_not_in_path_env_var(
);
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display("Can't create the symlink: {}", source))]
#[error("Can't create the symlink: {}", source)]
SymlinkingCreationIssue { source: std::io::Error },
#[snafu(display("Can't read the symlink: {}", source))]
SymlinkReadFailed { source: std::io::Error },
#[snafu(display("{}", source))]
#[error(transparent)]
InstallError { source: <Install as Command>::Error },
#[snafu(display("Can't get locally installed versions: {}", source))]
#[error("Can't get locally installed versions: {}", source)]
VersionListingError { source: installed_versions::Error },
#[snafu(display("Requested version {} is not currently installed", version))]
#[error("Requested version {} is not currently installed", version)]
CantFindVersion { version: UserVersion },
#[snafu(display(
"Can't find version in dotfiles. Please provide a version manually to the command."
))]
CantInferVersion,
#[snafu(display(
#[error(transparent)]
CantInferVersion {
#[from]
source: InferVersionError,
},
#[error(
"{}\n{}\n{}",
"We can't find the necessary environment variables to replace the Node version.",
"You should setup your shell profile to evaluate `fnm env`, see https://github.com/Schniz/fnm#shell-setup on how to do this",
"Check out our documentation for more information: https://fnm.vercel.app"
))]
)]
FnmEnvWasNotSourced,
#[error("Can't create the multishell directory: {}", path.display())]
MultishellDirectoryCreationIssue { path: std::path::PathBuf },
}
#[derive(Debug, Error)]
pub enum InferVersionError {
#[error("Can't find version in dotfiles. Please provide a version manually to the command.")]
Local,
#[error("Could not find any version to use. Maybe you don't have a default version set?\nTry running `fnm default <VERSION>` to set one,\nor create a .node-version file inside your project to declare a Node.js version.")]
Recursive,
}

83
src/config.rs

@ -1,19 +1,14 @@ @@ -1,19 +1,14 @@
use crate::arch::Arch;
use crate::log_level::LogLevel;
use crate::outln;
use crate::path_ext::PathExt;
use colored::Colorize;
use crate::version_file_strategy::VersionFileStrategy;
use crate::{arch::Arch, version_switch_strategy::VersionSwitchStrategy};
use dirs::{data_dir, home_dir};
use std::sync::atomic::{AtomicBool, Ordering};
use structopt::StructOpt;
use url::Url;
static HAS_WARNED_DEPRECATED_BASE_DIR: AtomicBool = AtomicBool::new(false);
#[derive(StructOpt, Debug)]
#[derive(clap::Parser, Debug)]
pub struct FnmConfig {
/// https://nodejs.org/dist/ mirror
#[structopt(
#[clap(
long,
env = "FNM_NODE_DIST_MIRROR",
default_value = "https://nodejs.org/dist",
@ -23,7 +18,7 @@ pub struct FnmConfig { @@ -23,7 +18,7 @@ pub struct FnmConfig {
pub node_dist_mirror: Url,
/// The root directory of fnm installations.
#[structopt(
#[clap(
long = "fnm-dir",
env = "FNM_DIR",
global = true,
@ -34,16 +29,11 @@ pub struct FnmConfig { @@ -34,16 +29,11 @@ pub struct FnmConfig {
/// Where the current node version link is stored.
/// This value will be populated automatically by evaluating
/// `fnm env` in your shell profile. Read more about it using `fnm help env`
#[structopt(
long,
env = "FNM_MULTISHELL_PATH",
hide_env_values = true,
hidden = true
)]
#[clap(long, env = "FNM_MULTISHELL_PATH", hide_env_values = true, hide = true)]
multishell_path: Option<std::path::PathBuf>,
/// The log level of fnm commands
#[structopt(
#[clap(
long,
env = "FNM_LOGLEVEL",
default_value = "info",
@ -55,14 +45,39 @@ pub struct FnmConfig { @@ -55,14 +45,39 @@ pub struct FnmConfig {
/// Override the architecture of the installed Node binary.
/// Defaults to arch of fnm binary.
#[structopt(
#[clap(
long,
env = "FNM_ARCH",
default_value,
default_value_t,
global = true,
hide_env_values = true
hide_env_values = true,
hide_default_value = true
)]
pub arch: Arch,
/// A strategy for how to resolve the Node version. Used whenever `fnm use` or `fnm install` is
/// called without a version, or when `--use-on-cd` is configured on evaluation.
///
/// * `local`: Use the local version of Node defined within the current directory
///
/// * `recursive`: Use the version of Node defined within the current directory and all parent directories
#[clap(
long,
env = "FNM_VERSION_FILE_STRATEGY",
possible_values = VersionFileStrategy::possible_values(),
default_value = "local",
global = true,
hide_env_values = true,
)]
version_file_strategy: VersionFileStrategy,
#[clap(
env = "FNM_VERSION_SWITCH_STRATEGY",
hide = true,
possible_values = VersionSwitchStrategy::possible_values(),
default_value = "path-symlink"
)]
version_switch_strategy: VersionSwitchStrategy,
}
impl Default for FnmConfig {
@ -73,11 +88,17 @@ impl Default for FnmConfig { @@ -73,11 +88,17 @@ impl Default for FnmConfig {
multishell_path: None,
log_level: LogLevel::Info,
arch: Arch::default(),
version_file_strategy: VersionFileStrategy::default(),
version_switch_strategy: VersionSwitchStrategy::default(),
}
}
}
impl FnmConfig {
pub fn version_file_strategy(&self) -> &VersionFileStrategy {
&self.version_file_strategy
}
pub fn multishell_path(&self) -> Option<&std::path::Path> {
match &self.multishell_path {
None => None,
@ -102,24 +123,6 @@ impl FnmConfig { @@ -102,24 +123,6 @@ impl FnmConfig {
let modern = data_dir().map(|dir| dir.join("fnm"));
if let Some(dir) = legacy {
if !HAS_WARNED_DEPRECATED_BASE_DIR.load(Ordering::SeqCst) {
HAS_WARNED_DEPRECATED_BASE_DIR.store(true, Ordering::SeqCst);
let legacy_str = dir.display().to_string();
let modern_str = modern.map_or("$XDG_DATA_HOME/fnm".to_string(), |path| {
path.display().to_string()
});
outln!(
self, Error,
"{}\n It looks like you have the {} directory on your disk.\n fnm is migrating its default storage location for application data to {}.\n You can read more about it here: {}\n",
"warning:".yellow().bold(),
legacy_str.italic(),
modern_str.italic(),
"https://github.com/schniz/fnm/issues/357".italic()
);
}
return dir;
}
@ -144,6 +147,10 @@ impl FnmConfig { @@ -144,6 +147,10 @@ impl FnmConfig {
.ensure_exists_silently()
}
pub fn version_switch_strategy(&self) -> &VersionSwitchStrategy {
&self.version_switch_strategy
}
#[cfg(test)]
pub fn with_base_dir(mut self, base_dir: Option<std::path::PathBuf>) -> Self {
self.base_dir = base_dir;

18
src/current_version.rs

@ -1,10 +1,11 @@ @@ -1,10 +1,11 @@
use thiserror::Error;
use crate::config::FnmConfig;
use crate::system_version;
use crate::version::Version;
use snafu::{OptionExt, ResultExt, Snafu};
pub fn current_version(config: &FnmConfig) -> Result<Option<Version>, Error> {
let multishell_path = config.multishell_path().context(EnvNotApplied)?;
let multishell_path = config.multishell_path().ok_or(Error::EnvNotApplied)?;
if multishell_path.read_link().ok() == Some(system_version::path()) {
return Ok(Some(Version::Bypassed));
@ -19,20 +20,21 @@ pub fn current_version(config: &FnmConfig) -> Result<Option<Version>, Error> { @@ -19,20 +20,21 @@ pub fn current_version(config: &FnmConfig) -> Result<Option<Version>, Error> {
.expect("Can't get filename")
.to_str()
.expect("Invalid OS string");
let version = Version::parse(file_name).context(VersionError { version: file_name })?;
let version = Version::parse(file_name).map_err(|source| Error::VersionError {
source,
version: file_name.to_string(),
})?;
Ok(Some(version))
} else {
Ok(None)
}
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[snafu(display(
"`fnm env` was not applied in this context.\nCan't find fnm's environment variables"
))]
#[error("`fnm env` was not applied in this context.\nCan't find fnm's environment variables")]
EnvNotApplied,
#[snafu(display("Can't read the version as a valid semver"))]
#[error("Can't read the version as a valid semver")]
VersionError {
source: semver::Error,
version: String,

9
src/default_version.rs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
use crate::config::FnmConfig;
use crate::version::Version;
use std::str::FromStr;
pub fn find_default_version(config: &FnmConfig) -> Option<Version> {
let version_path = config.default_version_dir().canonicalize().ok()?;
let file_name = version_path.parent()?.file_name()?;
Version::from_str(file_name.to_str()?).ok()?.into()
}

26
src/directories.rs

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
use std::path::PathBuf;
fn xdg_dir(env: &str) -> Option<PathBuf> {
let env_var = std::env::var(env).ok()?;
Some(PathBuf::from(env_var))
}
fn state_dir() -> Option<PathBuf> {
xdg_dir("XDG_STATE_HOME").or_else(dirs::state_dir)
}
fn cache_dir() -> Option<PathBuf> {
xdg_dir("XDG_CACHE_HOME").or_else(dirs::cache_dir)
}
fn runtime_dir() -> Option<PathBuf> {
xdg_dir("XDG_RUNTIME_DIR").or_else(dirs::runtime_dir)
}
pub fn multishell_storage() -> PathBuf {
runtime_dir()
.or_else(state_dir)
.or_else(cache_dir)
.unwrap_or_else(std::env::temp_dir)
.join("fnm_multishells")
}

65
src/downloader.rs

@ -4,38 +4,34 @@ use crate::archive::{Error as ExtractError, Extract}; @@ -4,38 +4,34 @@ use crate::archive::{Error as ExtractError, Extract};
use crate::directory_portal::DirectoryPortal;
use crate::version::Version;
use log::debug;
use snafu::{ensure, OptionExt, ResultExt, Snafu};
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error;
use url::Url;
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
HttpError {
#[from]
source: crate::http::Error,
},
#[error(transparent)]
IoError {
#[from]
source: std::io::Error,
},
#[snafu(display("Can't extract the file: {}", source))]
#[error("Can't extract the file: {}", source)]
CantExtractFile {
#[from]
source: ExtractError,
},
#[snafu(display("The downloaded archive is empty"))]
#[error("The downloaded archive is empty")]
TarIsEmpty,
#[snafu(display(
"{} for {} not found upstream.\nYou can `fnm ls-remote` to see available versions or try a different `--arch`.",
version,
arch
))]
VersionNotFound {
version: Version,
arch: Arch,
},
#[snafu(display("Version already installed at {:?}", path))]
VersionAlreadyInstalled {
path: PathBuf,
},
#[error("{} for {} not found upstream.\nYou can `fnm ls-remote` to see available versions or try a different `--arch`.", version, arch)]
VersionNotFound { version: Version, arch: Arch },
#[error("Version already installed at {:?}", path)]
VersionAlreadyInstalled { path: PathBuf },
}
#[cfg(unix)]
@ -75,7 +71,7 @@ pub fn extract_archive_into<P: AsRef<Path>>( @@ -75,7 +71,7 @@ pub fn extract_archive_into<P: AsRef<Path>>(
let extractor = archive::TarXz::new(response);
#[cfg(windows)]
let extractor = archive::Zip::new(response);
extractor.extract_into(path).context(CantExtractFile)?;
extractor.extract_into(path)?;
Ok(())
}
@ -88,23 +84,22 @@ pub fn install_node_dist<P: AsRef<Path>>( @@ -88,23 +84,22 @@ pub fn install_node_dist<P: AsRef<Path>>(
) -> Result<(), Error> {
let installation_dir = PathBuf::from(installations_dir.as_ref()).join(version.v_str());
ensure!(
!installation_dir.exists(),
VersionAlreadyInstalled {
path: installation_dir
if installation_dir.exists() {
return Err(Error::VersionAlreadyInstalled {
path: installation_dir,
});
}
);
std::fs::create_dir_all(installations_dir.as_ref()).context(IoError)?;
std::fs::create_dir_all(installations_dir.as_ref())?;
let temp_installations_dir = installations_dir.as_ref().join(".downloads");
std::fs::create_dir_all(&temp_installations_dir).context(IoError)?;
std::fs::create_dir_all(&temp_installations_dir)?;
let portal = DirectoryPortal::new_in(&temp_installations_dir, installation_dir);
let url = download_url(node_dist_mirror, version, arch);
debug!("Going to call for {}", &url);
let response = crate::http::get(url.as_str()).context(HttpError)?;
let response = crate::http::get(url.as_str())?;
if response.status() == 404 {
return Err(Error::VersionNotFound {
@ -117,17 +112,15 @@ pub fn install_node_dist<P: AsRef<Path>>( @@ -117,17 +112,15 @@ pub fn install_node_dist<P: AsRef<Path>>(
extract_archive_into(&portal, response)?;
debug!("Extraction completed");
let installed_directory = std::fs::read_dir(&portal)
.context(IoError)?
let installed_directory = std::fs::read_dir(&portal)?
.next()
.context(TarIsEmpty)?
.context(IoError)?;
.ok_or(Error::TarIsEmpty)??;
let installed_directory = installed_directory.path();
let renamed_installation_dir = portal.join("installation");
std::fs::rename(installed_directory, renamed_installation_dir).context(IoError)?;
std::fs::rename(installed_directory, renamed_installation_dir)?;
portal.teleport().context(IoError)?;
portal.teleport()?;
Ok(())
}
@ -159,13 +152,11 @@ mod tests { @@ -159,13 +152,11 @@ mod tests {
#[test_log::test]
fn test_installing_npm() {
let installations_dir = tempdir().unwrap();
let npm_path = install_in(installations_dir.path()).join(if cfg!(windows) {
"npm.cmd"
} else {
"npm"
});
let bin_dir = install_in(installations_dir.path());
let npm_path = bin_dir.join(if cfg!(windows) { "npm.cmd" } else { "npm" });
let stdout = duct::cmd(npm_path.to_str().unwrap(), vec!["--version"])
.env("PATH", bin_dir)
.stdout_capture()
.run()
.expect("Can't run npm")

28
src/installed_versions.rs

@ -1,11 +1,11 @@ @@ -1,11 +1,11 @@
use crate::version::Version;
use snafu::{ResultExt, Snafu};
use std::path::Path;
use thiserror::Error;
pub fn list<P: AsRef<Path>>(installations_dir: P) -> Result<Vec<Version>, Error> {
let mut vec = vec![];
for result_entry in installations_dir.as_ref().read_dir().context(IoError)? {
let entry = result_entry.context(IoError)?;
for result_entry in installations_dir.as_ref().read_dir()? {
let entry = result_entry?;
if entry
.file_name()
.to_str()
@ -17,19 +17,25 @@ pub fn list<P: AsRef<Path>>(installations_dir: P) -> Result<Vec<Version>, Error> @@ -17,19 +17,25 @@ pub fn list<P: AsRef<Path>>(installations_dir: P) -> Result<Vec<Version>, Error>
let path = entry.path();
let filename = path
.file_name()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))
.context(IoError)?
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?
.to_str()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))
.context(IoError)?;
let version = Version::parse(filename).context(SemverError)?;
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?;
let version = Version::parse(filename)?;
vec.push(version);
}
Ok(vec)
}
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
pub enum Error {
IoError { source: std::io::Error },
SemverError { source: semver::Error },
#[error(transparent)]
IoError {
#[from]
source: std::io::Error,
},
#[error(transparent)]
SemverError {
#[from]
source: semver::Error,
},
}

14
src/log_level.rs

@ -22,6 +22,14 @@ impl LogLevel { @@ -22,6 +22,14 @@ impl LogLevel {
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Quiet => "quiet",
Self::Error => "error",
Self::Info => "info",
}
}
pub fn possible_values() -> &'static [&'static str; 4] {
&["quiet", "info", "all", "error"]
}
@ -29,11 +37,7 @@ impl LogLevel { @@ -29,11 +37,7 @@ impl LogLevel {
impl From<LogLevel> for &'static str {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Quiet => "quiet",
LogLevel::Info => "info",
LogLevel::Error => "error",
}
level.as_str()
}
}

4
src/main.rs

@ -29,10 +29,14 @@ mod system_version; @@ -29,10 +29,14 @@ mod system_version;
mod user_version;
mod user_version_reader;
mod version;
mod version_file_strategy;
mod version_files;
#[macro_use]
mod log_level;
mod default_version;
mod directories;
mod version_switch_strategy;
fn main() {
env_logger::init();

43
src/shell/bash.rs

@ -1,41 +1,54 @@ @@ -1,41 +1,54 @@
use crate::version_file_strategy::VersionFileStrategy;
use super::shell::Shell;
use indoc::indoc;
use indoc::{formatdoc, indoc};
use std::path::Path;
#[derive(Debug)]
pub struct Bash;
impl Shell for Bash {
fn to_structopt_shell(&self) -> structopt::clap::Shell {
structopt::clap::Shell::Bash
fn to_clap_shell(&self) -> clap_complete::Shell {
clap_complete::Shell::Bash
}
fn path(&self, path: &Path) -> String {
format!("export PATH={:?}:$PATH", path.to_str().unwrap())
fn path(&self, path: &Path) -> anyhow::Result<String> {
let path = path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Can't convert path to string"))?;
Ok(format!("export PATH={:?}:$PATH", path))
}
fn set_env_var(&self, name: &str, value: &str) -> String {
format!("export {}={:?}", name, value)
}
fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String {
indoc!(
fn use_on_cd(&self, config: &crate::config::FnmConfig) -> anyhow::Result<String> {
let autoload_hook = match config.version_file_strategy() {
VersionFileStrategy::Local => indoc!(
r#"
__fnm_use_if_file_found() {
if [[ -f .node-version || -f .nvmrc ]]; then
fnm use
fnm use --silent-if-unchanged
fi
}
"#
),
VersionFileStrategy::Recursive => r#"fnm use --silent-if-unchanged"#,
};
Ok(formatdoc!(
r#"
__fnm_use_if_file_found() {{
{autoload_hook}
}}
__fnmcd() {
__fnmcd() {{
\cd "$@" || return $?
__fnm_use_if_file_found
}
}}
alias cd=__fnmcd
__fnm_use_if_file_found
"#
)
.into()
"#,
autoload_hook = autoload_hook
))
}
}

39
src/shell/fish.rs

@ -1,36 +1,49 @@ @@ -1,36 +1,49 @@
use crate::version_file_strategy::VersionFileStrategy;
use super::shell::Shell;
use indoc::indoc;
use indoc::{formatdoc, indoc};
use std::path::Path;
#[derive(Debug)]
pub struct Fish;
impl Shell for Fish {
fn to_structopt_shell(&self) -> structopt::clap::Shell {
structopt::clap::Shell::Fish
fn to_clap_shell(&self) -> clap_complete::Shell {
clap_complete::Shell::Fish
}
fn path(&self, path: &Path) -> String {
format!("set -gx PATH {:?} $PATH;", path.to_str().unwrap())
fn path(&self, path: &Path) -> anyhow::Result<String> {
let path = path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Can't convert path to string"))?;
Ok(format!("set -gx PATH {:?} $PATH;", path))
}
fn set_env_var(&self, name: &str, value: &str) -> String {
format!("set -gx {name} {value:?};", name = name, value = value)
}
fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String {
indoc!(
fn use_on_cd(&self, config: &crate::config::FnmConfig) -> anyhow::Result<String> {
let autoload_hook = match config.version_file_strategy() {
VersionFileStrategy::Local => indoc!(
r#"
function _fnm_autoload_hook --on-variable PWD --description 'Change Node version on directory change'
status --is-command-substitution; and return
if test -f .node-version -o -f .nvmrc
fnm use
fnm use --silent-if-unchanged
end
"#
),
VersionFileStrategy::Recursive => r#"fnm use --silent-if-unchanged"#,
};
Ok(formatdoc!(
r#"
function _fnm_autoload_hook --on-variable PWD --description 'Change Node version on directory change'
status --is-command-substitution; and return
{autoload_hook}
end
_fnm_autoload_hook
"#
)
.into()
"#,
autoload_hook = autoload_hook
))
}
}

45
src/shell/infer/mod.rs

@ -1,34 +1,21 @@ @@ -1,34 +1,21 @@
use super::{Bash, Fish, PowerShell, Shell, WindowsCmd, Zsh};
use log::debug;
use std::ffi::OsStr;
use sysinfo::{ProcessExt, System, SystemExt};
mod unix;
pub fn infer_shell() -> Option<Box<dyn Shell>> {
let mut system = System::new();
let mut current_pid = sysinfo::get_current_pid().ok();
mod windows;
while let Some(pid) = current_pid {
system.refresh_process(pid);
if let Some(process) = system.process(pid) {
current_pid = process.parent();
let process_name = process
.exe()
.file_stem()
.and_then(OsStr::to_str)
.map(str::to_lowercase);
let sliced = process_name.as_ref().map(|x| &x[..]);
match sliced {
Some("sh" | "bash") => return Some(Box::from(Bash)),
Some("zsh") => return Some(Box::from(Zsh)),
Some("fish") => return Some(Box::from(Fish)),
Some("pwsh" | "powershell") => return Some(Box::from(PowerShell)),
Some("cmd") => return Some(Box::from(WindowsCmd)),
cmd_name => debug!("binary is not a supported shell: {:?}", cmd_name),
};
} else {
current_pid = None;
}
}
#[cfg(unix)]
pub use self::unix::infer_shell;
#[cfg(not(unix))]
pub use self::windows::infer_shell;
pub(self) fn shell_from_string(shell: &str) -> Option<Box<dyn super::Shell>> {
use super::{Bash, Fish, PowerShell, WindowsCmd, Zsh};
match shell {
"sh" | "bash" => return Some(Box::from(Bash)),
"zsh" => return Some(Box::from(Zsh)),
"fish" => return Some(Box::from(Fish)),
"pwsh" | "powershell" => return Some(Box::from(PowerShell)),
"cmd" => return Some(Box::from(WindowsCmd)),
cmd_name => log::debug!("binary is not a supported shell: {:?}", cmd_name),
};
None
}

121
src/shell/infer/unix.rs

@ -0,0 +1,121 @@ @@ -0,0 +1,121 @@
#![cfg(unix)]
use crate::shell::Shell;
use log::debug;
use std::io::{Error, ErrorKind};
use thiserror::Error;
#[derive(Debug)]
struct ProcessInfo {
parent_pid: Option<u32>,
command: String,
}
const MAX_ITERATIONS: u8 = 10;
pub fn infer_shell() -> Option<Box<dyn Shell>> {
let mut pid = Some(std::process::id());
let mut visited = 0;
while let Some(current_pid) = pid {
if visited > MAX_ITERATIONS {
return None;
}
let process_info = get_process_info(current_pid)
.map_err(|err| {
debug!("{}", err);
err
})
.ok()?;
let binary = process_info
.command
.trim_start_matches('-')
.split('/')
.last()?;
if let Some(shell) = super::shell_from_string(binary) {
return Some(shell);
}
pid = process_info.parent_pid;
visited += 1;
}
None
}
fn get_process_info(pid: u32) -> Result<ProcessInfo, ProcessInfoError> {
use std::io::{BufRead, BufReader};
use std::process::Command;
let buffer = Command::new("ps")
.arg("-o")
.arg("ppid,comm")
.arg(pid.to_string())
.stdout(std::process::Stdio::piped())
.spawn()?
.stdout
.ok_or_else(|| Error::from(ErrorKind::UnexpectedEof))?;
let mut lines = BufReader::new(buffer).lines();
// skip header line
lines
.next()
.ok_or_else(|| Error::from(ErrorKind::UnexpectedEof))??;
let line = lines
.next()
.ok_or_else(|| Error::from(ErrorKind::NotFound))??;
let mut parts = line.split_whitespace();
let ppid = parts.next().ok_or_else(|| ProcessInfoError::Parse {
expectation: "Can't read the ppid from ps, should be the first item in the table",
got: line.to_string(),
})?;
let command = parts.next().ok_or_else(|| ProcessInfoError::Parse {
expectation: "Can't read the command from ps, should be the second item in the table",
got: line.to_string(),
})?;
Ok(ProcessInfo {
parent_pid: ppid.parse().ok(),
command: command.into(),
})
}
#[derive(Debug, Error)]
enum ProcessInfoError {
#[error("Can't read process info: {source}")]
Io {
#[source]
#[from]
source: std::io::Error,
},
#[error("Can't parse process info output. {expectation}. Got: {got}")]
Parse {
got: String,
expectation: &'static str,
},
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::process::{Command, Stdio};
#[test]
fn test_get_process_info() -> anyhow::Result<()> {
let subprocess = Command::new("bash")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let process_info = get_process_info(subprocess.id());
let parent_pid = process_info.ok().and_then(|x| x.parent_pid);
assert_eq!(parent_pid, Some(std::process::id()));
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save