Repository navigation
fix: LTS lines from the index, UTF-16/BOM .nvmrc, .nvmrc after a cd; docs: a page of scenarios - #44
Merged
Merged
Conversation
docs/scenarios.md and its translation show the plugin in concrete cases: the nvm-sh/nvm commands next to vfox's, Windows PowerShell 5.1 from scratch, one Node.js for a whole team, several projects in one shell, another release in one window, vfox exec, mirrors and editors. Both READMEs say what the plugin is for: it does for vfox what nvm-sh/nvm does for POSIX shells, and it also works in PowerShell, where nvm-sh/nvm does not run. Writing the cases turned up three things that the pages got wrong or left out: - .nvmrc and .node-version are read when a shell starts in their directory, not when you cd into it from a running shell. vfox 1.0.13 keeps the first result of `vfox env` and works it out again only for another .vfox.toml, a changed PATH or a newer config file, and a directory with only a .nvmrc changes none of these. `vfox --debug env` shows the plugin parsing the file and vfox then printing "Using cached output". Checked in bash 5.3 and PowerShell 7.6.9. A .vfox.toml switches on cd. nvmrc.md gets a section for it, and the READMEs and troubleshooting.md stop saying "when you enter a directory". - A .nvmrc with a byte order mark, or saved as UTF-16, selects nothing and vfox says nothing. Windows PowerShell 5.1 writes both: > and Out-File give UTF-16, -Encoding UTF8 gives UTF-8 with a mark. nvmrc.md lists it under "Not understood", and scenarios.md shows what to write instead. - Windows PowerShell 5.1 is not driven by the e2e suite, which runs PowerShell 7. platforms.md says so, and scenarios.md marks what rests on Microsoft's documentation or on vfox's source instead of on a run. docs/changes.md gets an "Unreleased" entry, with its translation.
The index of nodejs.org has marked 26.13.0 with the codename Lithium since 2026-10-28, but lts.md still said that no codename was announced and showed 24.22.0 as the newest LTS release. It now shows today's `vfox search nodejs | grep -m1 '(LTS)'`, and what the strategies do with Lithium on 2026-11-03: latest_available selects 26.13.0 for lts/* and lts/lithium, while latest_installed selects 24.22.2 for lts/* and nothing for lts/lithium, because the codename table in hooks/parse_legacy_file.lua has no entry for 26 yet. The table itself is not changed here, docs/development.md has the steps.
Node.js 26 entered long-term support on 2026-10-28 as Lithium. The table in hooks/parse_legacy_file.lua ended at Krypton (24), so under `specified` and `latest_installed` `lts/lithium` selected nothing and `lts` and `lts/*` selected the newest 24.x, while `latest_available`, which reads the codename from the index, selected 26.13.0 for all three. Add `lithium = 26`. The hook tests cover `lts/lithium` and the bare `lts`, which is the highest line of the table, and the table of the e2e suite (LTSCodenames) and its test follow. With 22.23.4, 24.21.0 and 26.13.0 installed, `lts/lithium` and `lts` now select 26.13.0 under `latest_installed` as well, checked against vfox 1.0.13.
…mark Windows PowerShell 5.1 writes UTF-16 with a byte order mark for `>`, `>>` and `Out-File`, and UTF-8 with a mark for `-Encoding UTF8`. ParseLegacyFile removed the white space of the file and read the bytes as they were, so the mark, or the zero bytes of UTF-16, became part of the text. No release matched it, and vfox said nothing: the file selected nothing. util.decode_text turns UTF-16 with a mark, in either byte order, into UTF-8 (surrogate pairs included, a lone surrogate becomes U+FFFD, a half unit at the end is dropped) and drops the UTF-8 mark. Text that starts with no mark is returned as it is, so UTF-32 and UTF-16 without a mark stay unread. Checked with the bytes of each on Linux against vfox 1.0.13: ASCII with LF, CRLF or no line break and the three marked encodings select 22.23.4, an empty file and a mark alone select nothing. The Windows e2e writes the pinned version with `-Encoding Unicode` and `utf8BOM`, which PowerShell 7 writes as 5.1 does for `>` and `-Encoding UTF8`, and probes both.
vfox keeps the result of `vfox env` for a shell in env-state.json in the session directory ($__VFOX_CURTMPPATH). It calculates again only for another .vfox.toml, a changed PATH or a newer config file, in vfox 1.0.13 and in `main` alike. A .nvmrc or .node-version is not tracked, so after a `cd` into a directory that has only one, vfox went on with the result it had made for the directory before, and the file was ignored. It was read when the shell started in the directory and after a `vfox use`, not after a cd. vfox runs ParseLegacyFile before it loads that file, so the hook now removes it (util.forget_env_state) whenever it reads a version file. vfox then calculates again at every prompt in such a directory: a `cd` into it switches, a cd from one to another switches, and a change of the file takes effect at the next prompt. In the test below the extra calculation cost about a millisecond per prompt with one SDK installed, and `--debug` shows `Config changed: true` there and `Using cached output` elsewhere. It is a workaround, to drop when vfox tracks version files. It cannot help on the way out: in a directory with no config at all the hook is not called, so vfox keeps the result of the directory that was left and the shell stays on its release. A directory with a .vfox.toml or a version file of its own switches, and so does a new shell. A fix in vfox would add the working directory to the state it keeps. Checked against vfox 1.0.13 in bash 5.3 and PowerShell 7.6.9, calling the `prompt` function that the hook installs once per command, with 26.13.0 as the global release: a cd into a .nvmrc for 18.20.8 selects it, then a .node-version for 24.22.2, an edit of that file to 22.23.4 takes effect, a cd into a .vfox.toml selects it and a cd into a directory with nothing goes back to 26.13.0. Release 0.5.3 stays on 26.13.0 in all but the .vfox.toml step. The Windows e2e no longer removes env-state.json itself before each probe, so that it covers this.
The three fixes before this commit change what the pages say, and each page marks what only `main` does with "Unreleased" and says what release 0.5.3 does, as docs/development.md asks. - nvmrc.md: "When the file is read" now says that vfox keeps its first result and does not track version files, that the plugin makes up for it on the way in and cannot on the way out, and shows one shell step by step under release 0.5.3 and under `main`, with the `--debug` lines of both sides. "What the file may hold" says that UTF-16 and a byte order mark are decoded, and its table of the three strategies is taken again on 2026-11-03 with 20.20.2, 22.23.4, 24.21.0, 25.9.0 and 26.13.0 installed, with a row for `lts/lithium`. - lts.md: Lithium in the table, and the "Last release" column taken from the index of 2026-11-03. The examples run again with 22.23.4, 24.21.0 and 26.13.0 installed, since `lts` now means 26. - scenarios.md: the cases for several projects and for the encodings of Windows PowerShell 5.1 show release 0.5.3 and `main` side by side, every cell taken from a run of each. - README, troubleshooting.md, development.md, changes.md and e2e.md follow, with the steps for the next promotion of an LTS line, the gap on the way out of a directory, and the Windows probes. The Chinese pages follow the English ones.
…e does docs/development.md named the fix of the way out of a directory with a version file as something vfox would have to do. It has been tried now: a build of vfox 1.0.13 that keeps the working directory in env-state.json as well (ConfigState.DirChanged, used by envFlag, 48 lines with a test) switches in every step of the shell in docs/nvmrc.md, the way out included, also with release 0.5.3 of the plugin, which has no workaround. The pages say so.
…un in ParseLegacyFile now removes $__VFOX_CURTMPPATH/env-state.json (util.forget_env_state), and the tests call it with the real os.getenv and os.remove. Run in a shell where vfox is active, which is the shell of anyone who works on a plugin, the 63 tests deleted the env-state.json of that shell: with __VFOX_CURTMPPATH pointing at a directory that held one, the file was gone afterwards. It is only a cache, vfox makes it again at the next prompt, but a test must not change the environment it runs in. The test file hides the variable from every test, and the tests that look at the removal set it themselves. Run again with the variable set, the file is still there, and the 63 tests pass with and without it. CI has no such shell, which is why it did not show.
Since 7a6fa78 docs/development.md has said that a build of vfox which keeps the working directory in env-state.json switches on the way out of a directory with a version file. The patch behind that was a file next to a checkout. It is docs/patches/vfox-1.0.13-env-state-working-dir.patch now, with a header that says what it is for, and the pages link to it. The licence check ignores *.patch: a patch for another project has to apply as it is. The pages say how to build a vfox with it (git clone --depth 1 --branch v1.0.13, git apply, go test, go build). Run as written on 2026-11-03, git apply is ok, the tests of internal/env and cmd pass, and the build reports 1.0.13. Replaying the shell of docs/scenarios.md step by step, with 26.13.0 as the global release: - vfox 1.0.13 as released with the plugin of this branch stays on 24.22.2 after the cd into a directory with no config at all (the known gap); - with the patch, the plugin of this branch and release 0.5.3 both go back to 26.13.0, and every other step is right as well; - an edit of a .node-version in the directory the shell is in takes effect at the next prompt with the plugin of this branch, patched vfox or not, and does not with release 0.5.3 on the patched vfox. So the patch does not make util.forget_env_state needless: the patch covers the cd in both directions, the workaround the edit in place. Corrections to 7a6fa78, whose message cannot be amended: the patch adds 55 lines, not 48; the replayed shell is the one of docs/scenarios.md, while docs/nvmrc.md has one more step, an edit of the file, which the patch alone does not cover; and the patch does not apply to vfox main as it is. On cdf0747 (2026-11-02) git apply refuses two of the three files and patch -p1 skips one hunk, because main has added a check for the PATH policy (PathPolicyChanged, SetPathPolicy) where the patch inserts its lines. A build from main was not tried.
Brings in #45 (73cba8b): the download of the package sends the User-Agent of vfox, so Huawei Cloud, TUNA, USTC and BFSU work as mirrors. The conflicts were in the "Unreleased" lists of docs/changes.md and docs/zh-CN/changes.md, where both sides added entries. Both are kept, the entry of #45 first because it reached main first. The other files that both sides changed (lib/util.lua, tests/hooks_test.lua, docs/development.md, docs/troubleshooting.md and their Chinese pages) merged clean as text, so the result was checked: - the 63 hook tests of this branch and the 2 of #45 pass, 65 in all, and with __VFOX_CURTMPPATH pointing at a directory that holds an env-state.json the tests leave the file alone; - StyLua 2.5.2 --check . and LuaLS 3.19.1 --checklevel=Warning are clean; - every relative link and heading anchor of the 30 pages resolves; - hooks/pre_install.lua, docs/mirror.md and docs/zh-CN/mirror.md, which only #45 changed, are identical to origin/main.
go test replays the result of an earlier run when what the test opened looks unchanged. A run of the Linux suite for the merge of main, after lib/util.lua and hooks/pre_install.lua had changed since the run before, printed `ok ... (cached)` with the output and the timings of that run, 28.92s and 138.63s again, to the hundredth. It would have passed whatever the plugin did. docs/e2e.md and its Chinese page now say to add -count=1 after editing hooks/ or lib/. CI starts from a clean checkout and is not affected.
"Windows container (windows-latest)" of this pull request (run 41612312368, 2c02b48) ended about two minutes in with FAIL docker pull mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-amd64 exited with code 1 The pull, which run.ps1 starts in the background before anything else, printed one line: "failed to connect to the docker API at npipe:////./pipe/docker_engine; check if the path is correct and if the daemon is running: open //./pipe/docker_engine: The system cannot find the file specified." The tools were fetched and vfox@main was built without trouble meanwhile, and the job failed when run.ps1 collected the pull. The engine was not listening yet. Nothing in this pull request touches the Windows suite, and the two runs of the same job before it (0898bbb, 8a7a881) passed with the same script on the same runner image (windows-2025-vs2026 20261030.294.1). In those the pull began 10.9 and 16.0 seconds after the log did, in the failed one 9.8 seconds, which fits an engine that comes up some ten seconds after the machine; that is a reading of the log, not something I could reproduce. run.ps1 now calls Wait-Engine before it starts the pull: `docker version` every five seconds, up to 60 times, the wait that install-docker-arm64.ps1 already has for the engine it starts. An engine that is up costs one call. One that never answers fails after five minutes with its own message, instead of at the end of the run with the exit code of the pull. docs/e2e.md and its Chinese page say so. Tested with PowerShell 7.6.9 on Linux and a fake docker, on the function cut out of run.ps1 under the settings that run.ps1 has at its top: an engine that answers at once (1 call), one that answers on the third call (returns after 2 s), one that never answers (throws "did not answer in 3 seconds" after 3 calls), and no docker at all (CommandNotFound); the preference variable is restored every time, and run.ps1 parses. Without the line that switches off the error preference for native commands, the second case throws on the first failed call. Not run on Windows: the next run of this job is the test.
5 tasks
The patch changed another project. Nothing in the plugin, its tests or CI used it, it applies to vfox 1.0.13 and not to main, and the only place where it can do some good is vfox. The problem is reported there now, with a reproduction, the cause in 1.0.13 and in main (cdf0747), and two ways to fix it: version-fox/vfox#745. It is tracked here as #46 until a vfox release fixes it. Removed: docs/patches/, the ignore for *.patch in .licenserc.yaml, the section "A vfox that notices a cd" of docs/development.md, and the sentences in nvmrc.md, scenarios.md, changes.md and development.md (Known gaps) that told how to build a vfox with it or counted on it, in English and Chinese. They say now that the cache is vfox's and link the two issues. What the pages say about the plugin does not change: util.forget_env_state covers the way into a directory with a version file and an edit in place, not the way out. The patch stays in the history of the branch (8a7a881) and not in the result of this pull request.
… a table lts/<codename>, lts and lts/* under `specified` and `latest_installed` came from a table in hooks/parse_legacy_file.lua, because ParseLegacyFile runs at every prompt in a directory with a version file and must not fetch index.json: 336 KB, 0.3 to 0.9 s here, and two requests for an lts/ name under latest_available. Measured with a logging mirror: 10 ms a prompt against 750 to 1000 ms. A table needs a release of the plugin for every line that Node.js promotes: the released plugin lacks Lithium since 2026-10-28, and asdf-nodejs, whose logic this was taken from, still ends at Krypton. nvm and fnm read the index. The plugin now writes what the index says into `.lts-cache` next to itself, and ParseLegacyFile reads that file: - Available calls util.remember_lts with the index it has decoded. - PreInstall calls util.refresh_lts_cache once the install has its checksum, so an install that fails early fetches nothing. vfox installs an exact release without asking Available (seen with a fresh HOME: the first hook call is PreInstall), so that is where such an install learns. It asks Available when the file is missing or a day old, and only when the file can be written, so a read-only directory does not cost an index per install. - ParseLegacyFile reads it through util.known_lts_lines: what was learned over the built-in util.LtsLines, and the newest major of the two for lts and lts/*. It never fetches. vfox gives a plugin no path of its own. util.plugin_dir finds the directory in package.path, where the plugin's hooks/?.lua and lib/?.lua are, and vfox keeps its own .available.cache in that directory too. The file is plain text (the json stub of the tests has no encode), written beside the old one and renamed over it. No directory, nothing writable, a missing or damaged file: the plugin has the table, as before. The table is the same twelve lines, now in util. Checked against the real vfox 1.0.13 in a scratch HOME: `vfox search` wrote the twelve lines of the live index, equal to the table. With a local mirror whose index has an invented v28.0.0, Mercury, a fake 28.0.0 installed and `lts/mercury` in a .nvmrc, latest_installed selects it with no request made by the prompts, and nothing without the file. A PreInstall of 20.20.2 fetched the index once, and not on the second run. 73 hook tests pass, 8 of them new, and each of 12 mutations of the code turns one of them red. StyLua and LuaLS are clean. gopher-lua 1.1.1 ends in a Go nil pointer dereference at `if file == nil` after `local file = path ~= nil and io.open(path, "r") or nil`. The code opens the file in two statements, and docs/development.md says so.
After the pinned install and `vfox search`, the Linux and Windows suites read
.lts-cache in the plugin directory (${VFOX_HOME:-$HOME/.vfox}/plugin/nodejs,
and C:\Users\John Doe\.vfox\plugin\nodejs in the Windows containers). They
require every codename of LTSCodenames, the Go copy of the plugin's table,
with the same major, and a newest LTS major of 26 or more. The index names
all of them, so this also tells when the table is wrong.
On Windows this is the first run of the code that finds the plugin directory
in a package.path with backslashes and renames a file over an existing one. I
cannot try that here, there is no Windows: if it fails there, the file is
missing and the job says which. common.LearnedLTS reads the file, with a unit
test.
The Linux suite passes locally (vfox latest, default mirror, -count=1): "PASS
the plugin learned 12 LTS lines from the index, the newest is 26".
lts.md tells how the plugin knows the codenames: the index under latest_available; under the other strategies what it learned, kept in .lts-cache next to the plugin; the table as the start. A promotion looks different now: the plugin knows the new line from the first search or install after it, with no release of the plugin, and lts.md still says what release 0.5.3 does. nvmrc.md, changes.md and the examples follow. development.md has a section on how the plugin learns: the file, the three places that use it, how the directory is found and how the tests keep away from the checkout. The steps for a promotion are housekeeping now, and the gopher-lua quirk that ended in a nil pointer dereference is written down next to the other one. e2e.md lists the new check. English and Chinese.
…t it shows on Windows The WSL suite did not call verifyLearnedLTS, although tests/e2e/wsl/verify.go asks to be changed together with the Linux one and docs/e2e.md listed the check for every combination. It does now: it reads .lts-cache in the VFOX_HOME of the slot and compares it with LTSCodenames, as the Linux suite does. Nothing here can run wsl.exe, so its first run is the one in CI. docs/e2e.md and the comment in tests/e2e/windows/verify.go said that the Windows check tries the path with backslashes and a rename over an existing file. The log of the run for 0c69b48 shows the file in C:\Users\John Doe\.vfox\plugin\nodejs with the twelve lines on all five Windows combinations (windows-latest three, windows-11-arm two), so the plugin finds its directory there, with a space in the path, and writes to it. It does not show whether the write that made the file replaced an earlier one: by the order of the steps the install writes the file and the search writes it again, but a rename that failed the second time would leave the first file, which passes the check as well. The text now says what the log shows, in English and Chinese.
linghengqian
added a commit
that referenced
this pull request
Nov 4, 2026
… LTS line (#47) * docs: list the 0.5.4 changes under the release and drop the "Unreleased" remarks Release 0.5.4 is published, so the pages stop marking its behavior as newer than the last release. This is the procedure of "Releasing" in docs/development.md, in English and Chinese: - changes.md: "Unreleased" becomes "0.5.4 (2026-11-04)", every entry gets its pull request (#43, #45, #44), the sentence about the newest release "at the time of writing" goes, and the User-Agent is `vfox/1.0.13 vfox-nodejs/0.5.4`. - README, README_CN: the paragraph that explains "Unreleased" goes, and the `.nvmrc` paragraph describes one behavior. - lts, nvmrc, scenarios, versions, mirror: the "Unreleased: release 0.5.3 ..." remarks go. The tables that set 0.5.3 against "Unreleased" (nvmrc.md, and two in scenarios.md) keep the one column that is 0.5.4. - troubleshooting, mirror, nvmrc: where a reader of an older plugin looks up a message or a symptom, a short "Before 0.5.4" / "Since 0.5.4" sentence stays (`version not found ... 27.0.0-alpha.0.0.0`, `checksum error` through Huawei Cloud, TUNA, USTC and BFSU, `.nvmrc` not applied after a `cd`). mirror.md no longer names the mirrors that worked before 0.5.4, only the four that failed, which is what was checked. - scenarios.md: the outputs are from "the code that became release 0.5.4" (hooks/ and lib/ of the PR branch equal the release), and the sentence that gave the fresh shell per directory of the Windows e2e suite as a result of the `cd` limit now says only what the suite covers. - source-build.md: the example URL of a released zip is 0.5.4. Checked with vfox 1.0.13 in an isolated HOME, using the published zip (sha256 0e2cbadc...): `vfox info` says 0.5.4. Against the live nodejs.org, `vfox search` lists 877 releases and no pre-release although the index starts with 27.0.0-alpha.0.0.0, and `.lts-cache` holds the 12 lines up to lithium. Against a local mirror whose index starts with a pre-release, `install nodejs@latest` takes the newest release, and the index, SHASUMS256.txt and the download all carry `User-Agent: vfox/1.0.13 vfox-nodejs/0.5.4`; with small stand-in tarballs, the 8-row table of nvmrc.md and the 7-row table of scenarios.md come out as written, and the three .nvmrc encodings select 22.23.4. The links and anchors of all pages resolve. * docs(versions): Node.js 26 is an LTS line since 2026-10-28 "A line that is not in long-term support yet, such as 26 today" stopped being true when 26.13.0 (Lithium) came out on 2026-10-28; the index of nodejs.org shows it with `lts: Lithium`, and it is the newest release, so `latest` is an LTS release now. The sentence is about why `latest` is not "the LTS", and the table above it is from 2026-10-21, when 26 was still the line that was not in long-term support. It says so now, in English and Chinese. This is not caused by release 0.5.4. It is a separate commit so that it can be dropped alone.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Seventeen commits, which can be read apart:
docs: add a page of scenarios, and say when .nvmrc is readdocs: say that Node.js 26 entered long-term support as Lithium(written before the fix and overtaken by 3 and 6, kept as it was pushed)fix(parse_legacy_file): add Lithium (Node.js 26) to the codename table(the table is only the cold start since 14)fix(parse_legacy_file): read a .nvmrc in UTF-16 or with a byte order markfix(parse_legacy_file): make vfox notice a version file after a cddocs: describe Lithium, UTF-16 files and version files after a cddocs: say what a build of vfox with the working directory in its state does(its message says 48 lines and the shell ofdocs/nvmrc.md; the patch adds 55 lines and the replayed shell is the one ofdocs/scenarios.md, see 9) (its text on the build is gone again with 13)test: keep the hook tests from removing the state of the shell they run indocs: keep the vfox patch in the repository, and say what it covers(taken out again by 13)Merge origin/main into docs/use-case-scenarios: brings in fix: download with vfox's User-Agent so Huawei Cloud, TUNA, USTC and BFSU work as mirrors #45 (Huawei Cloud, TUNA, USTC and BFSU as mirrors). The only conflicts were the two "Unreleased" lists ofdocs/changes.mdanddocs/zh-CN/changes.md, where both pull requests added entries; both are kept, the one of fix: download with vfox's User-Agent so Huawei Cloud, TUNA, USTC and BFSU work as mirrors #45 firstdocs: say that a second local run of the e2e suite can be a replaytest(e2e): wait for the Docker engine before the Windows suite pulls:Windows container (windows-latest)of the run for commit 11 failed because the Docker engine did not answer yet whenrun.ps1started its base image pull (failed to connect to the docker API at npipe:////./pipe/docker_engine). Nothing in this pull request touches the Windows suite, and the two runs before on the same runner image passed, and the run for the commit itself passed in fulldocs: take the vfox patch out of this pull request, and link the issues: the problem is reported in vfox as version-fox/vfox#745 and tracked here as Track version-fox/vfox#745: .nvmrc and .node-version are not applied after acd#46, which stays open until a vfox release has the change. The patch of commits 7 and 9 is not part of the result of this pull requestfix(parse_legacy_file): learn the LTS lines from the index instead of a table:lts/<codename>,ltsandlts/*no longer depend on a table that a release of the plugin has to extend for every line Node.js promotes. The plugin keeps what the index says in.lts-cachenext to itself and reads it offlinetest(e2e): check the file in which the plugin learned the LTS lines: the Linux and Windows suites read the file after the install and the search (the WSL suite follows in 17)docs: say that the plugin learns the LTS lines from the index, English and Chinesetest(e2e): run the check of the learned LTS lines on WSL, and say what it shows on Windows: the WSL suite had not run the check of 15, although itsverify.goasks to be changed together with the Linux one; anddocs/e2e.mdand a comment of the Windows suite said that the check tries a rename over an existing file, which the log does not show (see "Not checked")The pull request began as the page of scenarios. Writing its cases turned up three problems of the plugin. They are fixed here, one commit each (3 to 5), so that any of them can be dropped, and 6 carries the documentation of all three, in both languages. The table of LTS codenames, which had gone stale for Lithium, no longer needs anybody to keep it up: the plugin learns the lines from the index (14 to 17).
The page of scenarios
docs/scenarios.mdanddocs/zh-CN/scenarios.mdshow the plugin in concrete cases, and both READMEs say what it is for: it does for vfox what nvm-sh/nvm does for POSIX shells, and it also works in PowerShell, where nvm-sh/nvm does not run. Cases: the nvm commands next to vfox's; Windows PowerShell 5.1 from scratch (execution policy, profile, what differs on Windows); one Node.js for a whole team (.vfox.toml, or the.nvmrcthat nvm users already have,vfox install --all -y); several projects in one shell; another release in one window;vfox execfor a test matrix and for CI; following a line or pinning a release; mirrors and proxies; editors. Where release 0.5.3 andmaindiffer, the tables show both, every cell taken from a run of each.The fixes
LTS lines (3, 14). Node.js 26 entered long-term support on 2026-10-28 as Lithium. The codename table ended at Krypton, so under
latest_installedlts/lithiumselected nothing andltsandlts/*selected a 24.x, whilelatest_availableselected 26.13.0. Commit 3 addedlithium = 26, which is the stopgap that a table needs once a year.ParseLegacyFileruns at every prompt in a directory with a version file, so it cannot ask the index for the lines: 336 KB, and two requests for anlts/name, which took 750 to 1000 ms a prompt against 10 ms (measured with a logging mirror, nodejs.org from here; 270 to 380 ms through npmmirror). Commit 14 has the plugin write what the index says into.lts-cache, a text file next to the plugin where vfox keeps its own.available.cache, whenever it has the index in hand anyway:Available(vfox search, an install oflatestor of a major), and an install of an exact release when the file is missing or a day old (vfox installs those without askingAvailable).ParseLegacyFilereads the file, over the built-in table, and never fetches. A plugin directory that cannot be written, or a plugin that has learned nothing yet, has the table, as before. nvm and fnm read the index; asdf-nodejs, whose logic this was taken from, still ends its table at Krypton. The built-in table is the cold start, and a line missing from it is learned the first time the plugin has the index in hand.UTF-16 and a byte order mark (4). Windows PowerShell 5.1 writes UTF-16 with a mark for
>andOut-File, and UTF-8 with a mark for-Encoding UTF8. The plugin read the bytes as they were, so the mark became part of the text, no release matched it, and vfox said nothing.util.decode_textdecodes both, and leaves text with no mark as it is. UTF-32 and UTF-16 without a mark are still not read, and the docs say so.A
.nvmrcafter acd(5), a workaround. vfox 1.0.13 keeps the result ofvfox envfor a shell (env-state.jsonin the session directory) and calculates again only for another.vfox.toml, a changedPATHor a newer config file. A.nvmrcor.node-versionis not among these, andmainof vfox has the same code (internal/env/state.go, andconfigPathsincmd/commands/env.goholds only.vfox.tomlfiles that exist). So after acdfrom a running shell the file was ignored. vfox runsParseLegacyFilebefore it loads that state, and the hook now removesenv-state.jsonwhenever it reads a version file (util.forget_env_state). What that does and does not do, with the same shell replayed under release 0.5.3 and under this branch (global release 26.13.0):cdinto a directory with a.nvmrcfor 18.20.8cdinto a directory with a.node-versionfor 24.22.2cdinto a directory with no config at allcdinto a directory with a.vfox.tomlfor 18.20.8cdinto a directory with no config at all.vfox.tomlor a version file of its own switches, and so does a new shell. I found no way to do that from a plugin, and it needs a change in vfox. It is reported there as version-fox/vfox#745 (a reproduction with the 1.0.13 release and withmain, the cause, two ways to change it) and tracked here as Track version-fox/vfox#745: .nvmrc and .node-version are not applied after acd#46. A change that ties vfox's kept result to the working directory, which I tried on 1.0.13, switched on everycdof the shell above, the way out included, with the plugin of this branch and with release 0.5.3; it does not notice an edit of the file in the directory the shell is in (release 0.5.3 stayed on the old release), so commit 5 would stay useful for that. The patch itself is not part of this pull request.$__VFOX_CURTMPPATH, and it makes vfox calculate again at every prompt in a directory with a version file. The extra calculation took about a millisecond per prompt with one SDK installed (7.4 against 7.6 ms forvfox env, 30 calls each), and with many SDKs it will cost more. It is isolated in one function and one call, so dropping commit 5 is a revert, plus the lines of commit 6 that describe it. Say so if you would rather not carry it.How it was checked
forget_env_stateremoved theenv-state.jsonof that shell, since the tests used the realos.getenvandos.remove. CI has no such shell, so it did not show. The tests now hide__VFOX_CURTMPPATH, and with it set the file survives a run. StyLua 2.5.2--check .and LuaLS 3.19.1--checklevel=Warningare clean, with the release assets checked against the digests that GitHub publishes.gofmt,go vet(alsoGOOS=windowsfor the Windows suite) andgo test -shortintests/e2epass.HOMEwith a clean environment: the.nvmrcencodings (ASCII with LF, CRLF or no line break, UTF-8 with a mark, UTF-16 in both byte orders),lts/lithium,lts/*andltsunder the three strategies, and the shell above in bash 5.3 and in PowerShell 7.6.9 (the officiallinux-x64tarball, SHA-256 checked), calling thepromptfunction that the hook installs once per command.--debugshowsConfig changed: truein a directory with a version file andUsing cached outputafter leaving it.HOME:vfox searchwrote.lts-cachewith the twelve lines of the live index, equal to the built-in table, and left no temporary file. With a local mirror whose index has an inventedv28.0.0, Mercury, a fake 28.0.0 installed andlts/mercuryin a.nvmrc,latest_installedselects it and the prompts made no request; without the file it selects nothing. APreInstallof 20.20.2 fetched the index once and not on the second run. gopher-lua 1.1.1 ends in a Go nil pointer dereference atif file == nilafterlocal file = path ~= nil and io.open(path, "r") or nil; the code opens the file in two statements, anddocs/development.mdsays so.latest, default mirror, run locally withE2E_MAX_JOBS=1: pass, including the legacy probes and the new check of.lts-cache(commit 15; 208 s with-count=1), before the merge and again on the merge ofmain(78 s with-count=1). The first run on the merge printedok ... (cached)with the timings of the run before it, becausego testreplays a result when what the test opened looks unchanged, plugin files that had changed included. I threw that run away and ran it again with-count=1; commit 11 says so indocs/e2e.md. The rest of the matrix ran in CI, see the next item.28736b7(run 41628376081): every job passed, and so did the other checks (check,license-eye,lint-go,lint-lua,test). The check of.lts-cache(commits 15 and 17) printedPASS the plugin learned 12 LTS lines from the index, the newest is 26for all 17 combinations: Linux and Alpine on amd64 and arm64 (latestandmainvfox with the default mirror, andlatestwith npmmirror on amd64),windows-latest(three),windows-11-arm(two) and WSL (two). In the Windows containers the plugin is inC:\Users\John Doe\.vfox\plugin\nodejs, with a space in the path, andGet-Contentfound the file there with all twelve codenames at the majors of the table, so on Windows the plugin finds its directory and writes to it. The run for0c69b48, before commit 17, passed the same way on every job except WSL, which did not run the check yet.HOMEwith a clean environment and the nodejs plugin 0.5.3 from the registry (vfox add nodejs), with the 1.0.13 release binary and with a build ofmain(cdf0747): two bash sessions, one that goes into a.nvmrcdirectory and one that goes out of it. The output is identical for both, and the transcripts in the issue are that output. The seven-step shell ofdocs/scenarios.mdran on that build ofmaintoo, with release 0.5.3 of the plugin and with the plugin of this branch, and gave the same cells as with 1.0.13; the row of the table above that edits the file was only run on 1.0.13.scenarios.mdhave the same code blocks and the same inline code.Not checked
docker(an engine that answers at once, on the third call, never, and nodocker)..lts-cacheon Windows (commits 14 and 15). The Windows jobs show that the plugin finds its directory and writes there, see above. They do not show that a write replaces a file that is already there: by the order of the steps the install writes the file and the search writes it again, but a rename that failed the second time would leave the first file, and the check passes with that file too. The plugin takes it quietly, since the file would stay as old as the first write and an install after a day would fetch the index again, with no other harm. The only basis for it working is that Go'sos.Rename, whichos.renamecalls under gopher-lua, replaces an existing file on Windows. My first text fordocs/e2e.mdand a comment in the Windows suite said that the check tries this rename; commit 17 takes that back.cdin a running Windows shell. The Windows suite writes the pinned version with-Encoding Unicodeandutf8BOMand probes both, and no longer removesenv-state.jsonitself before a probe, so it relies on commit 5. Those probes passed onwindows-latest(three combinations) andwindows-11-arm(two) in the run for commit 12, and the two runs before it succeeded as well. But each probe starts a new PowerShell, so none of this is acdin a shell that keeps running, which is whatforget_env_stateis for, and it does not show that the file is removed on Windows paths (a removal that fails is ignored on purpose). PowerShell 7 writes the same bytes for these encodings as Windows PowerShell 5.1 does for>and-Encoding UTF8(checked on Linux).Path, junctions, the fallback to the global scope andvfox execwithnode.exerest on vfox's source.docs/scenarios.mdmarks each.vfox useis global on Windows. Its source defaults to the session when the hook is active. The pages spell out the scope in every Windows command.