Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ nodeup upgrade --lts
- **nodeup version:** `nodeup version` output
- **OS:** (e.g., macOS 14.4, Ubuntu 24.04, Windows 11)
- **Architecture:** (e.g., arm64, x86_64)
- **Version manager:** (fnm, nvm, Volta, asdf, mise, n, nodenv, nvm-windows)
- **Version manager:** (fnm, nvm, Volta, asdf, mise, n, nodenv, vite-plus, nvm-windows)
- **Manager version:** output of `fnm --version` (or equivalent)
- **Node version(s) installed:** output of `nodeup list`

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal/cli/ cobra command wiring — thin layer, delegates to int
check.go / list.go / packages.go / config.go / version.go
internal/detector/ Manager interface + one file per manager
detector.go DetectAll(), ResolveManager()
fnm.go nvm.go volta.go asdf.go mise.go n.go nodenv.go nvm_windows.go
fnm.go nvm.go volta.go asdf.go mise.go n.go nodenv.go viteplus.go nvm_windows.go
internal/node/
dist.go nodejs.org/dist/index.json client + 24h TTL cache
internal/packages/ npm global snapshot / restore / migrate (merged in PR #19)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ them over, and (optionally) cleans up — all interactively, all resumable.
## Features

- 🔍 **Auto-detects** your Node.js version manager: `fnm`, `nvm`, `Volta`,
`asdf`, `mise`, `n`, `nodenv`, `nvm-windows`
`asdf`, `mise`, `n`, `nodenv`, `vite-plus`, `nvm-windows`
- 📦 **Migrates global npm packages** across Node versions automatically
- 🌍 **Cross-platform**: macOS, Linux, Windows (incl. ARM64)
- 💾 **Single static binary** — no runtime, no npm dependency
Expand Down Expand Up @@ -250,7 +250,7 @@ v1.0.0 → v1.1.0 history.

| Phase | Status | What shipped |
|---|---|---|
| 1 — Detector engine | ✅ | 8/8 managers detected (`fnm`, `nvm`, `Volta`, `asdf`, `mise`, `n`, `nodenv`, `nvm-windows`) |
| 1 — Detector engine | ✅ | 9/9 managers detected (`fnm`, `nvm`, `Volta`, `asdf`, `mise`, `n`, `nodenv`, `vite-plus`, `nvm-windows`) |
| 2 — Node version API | ✅ | `nodeup check` + nodejs.org/dist/index.json fetch + TTL cache + HTTP retry/backoff/timeout |
| 3 — Package snapshot/restore | ✅ | `nodeup packages {snapshot,restore,diff}` + interrupted-upgrade sentinel + MigrationReport |
| 4 — Upgrade command | ✅ | End-to-end `nodeup upgrade` with snapshot → install → migrate → cleanup |
Expand Down
4 changes: 2 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ manual steps, single static binary, no Node runtime dependency.
`Install/Uninstall/Use/SetDefault(v semver.Version) error`,
`GlobalNpmPrefix(v semver.Version) (string, error)`,
`Current() (semver.Version, error)`
- 8 managers implementing `Manager`: fnm, nvm, volta, asdf, mise, n,
nodenv, nvm-windows
- 9 managers implementing `Manager`: fnm, nvm, volta, asdf, mise, n,
nodenv, vite-plus, nvm-windows

## §V Invariants

Expand Down
7 changes: 4 additions & 3 deletions cmd/nodeup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
//
// nodeup is an automated Node.js version upgrade + global package migration
// tool. It detects which Node.js version manager is installed (fnm, nvm,
// Volta, asdf, mise, n, nodenv, nvm-windows), fetches the latest LTS and
// Current versions from the nodejs.org release API, snapshots global npm
// packages, installs the new Node versions, and migrates the packages over.
// Volta, asdf, mise, n, nodenv, vite-plus, nvm-windows), fetches the latest
// LTS and Current versions from the nodejs.org release API, snapshots global
// npm packages, installs the new Node versions, and migrates the packages
// over.
package main

import (
Expand Down
14 changes: 8 additions & 6 deletions docs/managers.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Supported Version Managers

All 8 managers listed below are fully detected by `nodeup` as of v1.0.0
All 9 managers listed below are fully detected by `nodeup` as of v1.1.0
(macOS, Linux, and Windows where applicable). The matrix is the source of
truth — keep it in sync with `internal/detector/registry_*.go` and the
per-manager files (`fnm.go`, `nvm.go`, `volta.go`, `asdf.go`, `mise.go`,
`n.go`, `nodenv.go`, `nvm_windows.go`).
`n.go`, `nodenv.go`, `viteplus.go`, `nvm_windows.go`).

nodeup auto-detects the following version managers. Detection runs in
priority order — earlier managers win when multiple are installed.
Expand All @@ -18,6 +18,7 @@ priority order — earlier managers win when multiple are installed.
| [mise](https://mise.jdx.dev) | macOS, Linux | `mise` on PATH, `node` plugin |
| [n](https://github.com/tj/n) | macOS, Linux | `n` on PATH, `N_PREFIX` |
| [nodenv](https://github.com/nodenv/nodenv) | macOS, Linux | `nodenv` on PATH, `~/.nodenv/shims` |
| [Vite+](https://viteplus.dev) | macOS, Linux, Windows | `vp` on PATH, `VP_HOME`, `VP_DATA_DIR`, `~/.local/share/vite-plus` (macOS/Linux) / `%LOCALAPPDATA%\vite-plus\data` (Windows) |
| [nvm-windows](https://github.com/coreybutler/nvm-windows) | Windows only | `nvm.exe` on PATH, registry entry |

## The nvm special case
Expand Down Expand Up @@ -45,10 +46,10 @@ nodeup upgrade --manager fnm
## When `node` on PATH doesn't belong to a manager

nodeup's job is to swap Node versions inside a *version manager* it
manages — `fnm`, `nvm`, `Volta`, `asdf`, `mise`, `n`, `nodenv`, or
`nvm-windows`. If the `node` binary that lives first on your PATH
was installed some other way, nodeup can't safely replace it: the
other installer would just put it back on the next update.
manages — `fnm`, `nvm`, `Volta`, `asdf`, `mise`, `n`, `nodenv`,
`vite-plus`, or `nvm-windows`. If the `node` binary that lives first on
your PATH was installed some other way, nodeup can't safely replace it:
the other installer would just put it back on the next update.

Both `nodeup upgrade` and `nodeup check` classify the `node` on PATH
into one of these buckets:
Expand Down Expand Up @@ -147,6 +148,7 @@ Precedence (highest first):
| **mise** | `mise install node@<v>` | `mise uninstall node@<v>` | `mise current node` | `SetDefault` writes to `~/.config/mise/config.toml` via `mise use --global`. |
| **n** | `n install <v>` | `n uninstall <v>` | `n current` (n ≥ 8) | `SetDefault` is a no-op (n auto-uses the latest install). |
| **nodenv** | `nodenv install <v>` | `nodenv uninstall <v>` | `nodenv version` (treats `system` as "unknown") | SetDefault writes to `~/.nodenv/version`. |
| **vite-plus** | `vp env install node@<v>` | `vp env uninstall node@<v>` | `vp env current node --json` | Uses the `node@<v>` spec form. `Current()` reports "unknown" in Vite+'s `system_first` mode — the active Node is the system install there, not a Vite+-managed runtime — so cleanup falls back to per-version confirmation. |
| **nvm-windows**| **unsupported** | **unsupported** | returns `ErrNVMWindowsNotImplemented` | `nvm-windows` doesn't expose a CLI for install/uninstall — `Install`/`Uninstall`/`Use`/`SetDefault`/`GlobalNpmPrefix` all return `ErrNVMWindowsNotImplemented`; the upgrade command leaves the install list alone, and the cleanup prompt runs but each per-version attempt surfaces that sentinel. |

For nvm-windows, the cleanup prompt **still runs** — it just
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func newListCmd() *cobra.Command {
Use: "list",
Short: "List installed Node.js versions via the detected manager",
Long: `List installed Node.js versions via the detected version manager
(fnm, nvm, Volta, asdf, mise, n, nodenv, nvm-windows).
(fnm, nvm, Volta, asdf, mise, n, nodenv, vite-plus, nvm-windows).

With --json, emits a stable envelope consumable by jq/yq/etc. The
human-readable default prints one line per detected manager with
Expand All @@ -60,7 +60,7 @@ versions sorted ascending.`,
}

cmd.Flags().Bool("json", false, "output as JSON")
cmd.Flags().String("manager", "", "force a specific manager (fnm, nvm, volta, asdf, mise, n, nodenv, nvm-windows); default is auto-detect")
cmd.Flags().String("manager", "", "force a specific manager (fnm, nvm, volta, asdf, mise, n, nodenv, vite-plus, nvm-windows); default is auto-detect")

return cmd
}
Expand Down Expand Up @@ -143,7 +143,7 @@ func outputListTable(w ui.Writer, entries []installedEntryJSON) error {
w.Println("")
if len(entries) == 0 {
w.Info("No Node.js version manager detected.")
w.Info("Install one of: fnm, nvm, Volta, asdf, mise, n, nodenv, nvm-windows.")
w.Info("Install one of: fnm, nvm, Volta, asdf, mise, n, nodenv, vite-plus, nvm-windows.")
return nil
}
for _, e := range entries {
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ func NewRootCmd(version, commit, date string) *cobra.Command {
the manual churn.

It auto-detects your version manager (fnm, nvm, Volta, asdf, mise, n,
nodenv, nvm-windows), fetches the latest LTS and Current versions from
nodejs.org, installs them, and migrates your global npm packages
across so you don't lose anything.
nodenv, vite-plus, nvm-windows), fetches the latest LTS and Current
versions from nodejs.org, installs them, and migrates your global npm
packages across so you don't lose anything.

Common workflows:
nodeup upgrade Upgrade both LTS and Current
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ migrate your global npm packages across automatically.`,
cmd.Flags().Bool("no-cleanup", false, "skip the prompt to remove old versions")
cmd.Flags().Bool("cleanup", false, "auto-confirm the post-upgrade cleanup of old versions")
cmd.Flags().StringSlice("cleanup-version", nil, "specify version(s) to delete (repeatable; pairs with --cleanup)")
cmd.Flags().String("manager", "", "force a specific manager (fnm, nvm, volta, asdf, mise, n, nodenv)")
cmd.Flags().String("manager", "", "force a specific manager (fnm, nvm, volta, asdf, mise, n, nodenv, vite-plus)")
cmd.Flags().BoolP("yes", "y", false, "non-interactive, assume yes to all prompts")
cmd.Flags().Bool("offline", false, "use cached data, don't hit nodejs.org")

Expand Down
3 changes: 2 additions & 1 deletion internal/detector/detector.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Package detector discovers and abstracts Node.js version managers on the
// user's system: fnm, nvm, Volta, asdf, mise, n, nodenv, nvm-windows.
// user's system: fnm, nvm, Volta, asdf, mise, n, nodenv, vite-plus,
// nvm-windows.
//
// The public surface is the Manager interface and the DetectAll /
// ResolveManager helpers. Each concrete manager lives in its own file
Expand Down
1 change: 1 addition & 0 deletions internal/detector/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func All() []Manager {
NewMise(),
NewN(),
NewNodenv(),
NewVitePlus(),
}
}

Expand Down
1 change: 1 addition & 0 deletions internal/detector/registry_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func All() []Manager {
NewMise(),
NewN(),
NewNodenv(),
NewVitePlus(),
NewNVMWindows(),
}
}
Expand Down
9 changes: 9 additions & 0 deletions internal/detector/system_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,15 @@ func managerManagedRoots(m Manager) (roots []string, ok bool) {
return []string{filepath.Join(h, ".nodenv")}, true
}
return []string{}, true
case "vite-plus":
// Vite+ stores Node runtimes under its data directory:
// <data>/js_runtime/node/<v>/. The data directory resolves
// through VP_HOME → <home>/.vite-plus → VP_DATA_DIR → the
// XDG/platform default, so we hand back every candidate
// rather than just the first hit — the shell nodeup runs in
// may not carry the same Vite+ env vars that `vp` would
// resolve. See vitePlusDataDirs in viteplus.go.
return vitePlusDataDirs(), true
}
// nvm-windows, scoop, and any unknown manager: no recognized
// root pattern; fall through to path-based classification.
Expand Down
35 changes: 35 additions & 0 deletions internal/detector/system_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"testing"

"github.com/Masterminds/semver/v3"

"github.com/dipto0321/nodeup/internal/platform"
)

// --- SystemNodeKind.String --------------------------------------------------
Expand Down Expand Up @@ -420,6 +422,7 @@ func TestManagerManagedRoots(t *testing.T) {
for _, v := range []string{
"FNM_DIR", "NVM_DIR", "VOLTA_HOME", "ASDF_DIR", "ASDF_DATA_DIR",
"MISE_DATA_DIR", "N_PREFIX", "NODENV_ROOT",
"VP_HOME", "VP_DATA_DIR", "LOCALAPPDATA",
"XDG_DATA_HOME",
"FNM_MULTISHELL_PATH", "XDG_STATE_HOME", "TMPDIR",
} {
Expand All @@ -431,6 +434,15 @@ func TestManagerManagedRoots(t *testing.T) {
// expects it as a suffix.
fnmStateRoot := filepath.Join("/home/tester", ".local", "state", "fnm_multishells")

// The Vite+ platform-default data dir (no VP_HOME / VP_DATA_DIR
// set). The legacy <home>/.vite-plus candidate only appears when
// the directory holds a "current" link, which /home/tester does
// not, so it stays out of the expectations.
vitePlusDefaultRoot := filepath.Join("/home/tester", ".local", "share", "vite-plus")
if platform.IsWindows() {
vitePlusDefaultRoot = filepath.Join("/home/tester", "AppData", "Local", "vite-plus", "data")
}

cases := []struct {
name string
mgr string
Expand Down Expand Up @@ -569,6 +581,29 @@ func TestManagerManagedRoots(t *testing.T) {
wantOK: false,
description: "we don't recognize the manager, so the classifier falls through to path patterns",
},
{
name: "vite-plus env wins",
mgr: "vite-plus",
envKey: "VP_HOME", envVal: "/srv/vite-plus",
wantRoots: []string{"/srv/vite-plus", vitePlusDefaultRoot},
wantOK: true,
description: "VP_HOME override, followed by the platform default candidate",
},
{
name: "vite-plus data dir override",
mgr: "vite-plus",
envKey: "VP_DATA_DIR", envVal: "/srv/vite-plus-data",
wantRoots: []string{"/srv/vite-plus-data", vitePlusDefaultRoot},
wantOK: true,
description: "VP_DATA_DIR override, followed by the platform default candidate",
},
{
name: "vite-plus falls back to the platform default",
mgr: "vite-plus",
wantRoots: []string{vitePlusDefaultRoot},
wantOK: true,
description: "no env → the XDG / platform data dir",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down
Loading
Loading