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
50 changes: 41 additions & 9 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# - checksums.txt with SHA256 for every artifact (the 6th release
# file, but it's a checksums manifest, not a binary)
# - A GitHub Release titled `v<version>` with changelog and all artifacts
# - A homebrew formula pushed to github.com/dipto0321/homebrew-tap
# - A homebrew cask pushed to github.com/dipto0321/homebrew-tap
# - A scoop manifest pushed to github.com/dipto0321/scoop-bucket

version: 2
Expand All @@ -36,7 +36,23 @@ builds:
- CGO_ENABLED=0
flags:
- -trimpath
- -ldflags=-s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.CommitDate}}
# The ldflags live in the dedicated `ldflags` key, NOT in `flags`.
#
# GoReleaser has its own default ldflags (`-s -w -X main.version=... -X
# main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser`)
# and it only applies them when `builds[].ldflags` is empty. Passing
# `-ldflags=...` through `flags` instead produced two `-ldflags`
# arguments on the `go build` command line — Go took the last one, so
# our `-X main.commit={{.ShortCommit}} -X main.date={{.CommitDate}}`
# was silently dropped and the shipped binary reported GoReleaser's
# defaults instead (a full 40-char commit and the build date rather
# than the commit date). GoReleaser also logs "ldflags is defined
# twice" to warn about exactly this.
#
# Using `ldflags:` keeps a single, explicit ldflags argument and
# preserves the short-commit / commit-date metadata we want.
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.CommitDate}}
goos:
- linux
- windows
Expand All @@ -51,10 +67,16 @@ builds:

archives:
- id: default
format: tar.gz
# `formats` (plural, list form) replaced the deprecated singular
# `format` in GoReleaser v2.6. The archive names are unchanged —
# `nodeup_<version>_<os>_<arch>.tar.gz` — which is what the npm
# wrapper's archiveName() and the Homebrew/Scoop manifests expect.
formats:
- tar.gz
format_overrides:
- goos: windows
format: zip
formats:
- zip
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
files:
- LICENSE
Expand Down Expand Up @@ -95,7 +117,19 @@ changelog:
order: 999

# Homebrew tap — auto-pushed on every release. Requires HOMEBREW_TAP_TOKEN.
brews:
#
# GoReleaser deprecated the `brews:` (formula) section in v2.10 in favour of
# `homebrew_casks:`. A formula's `install:` block is raw Ruby; a cask's
# `binary:` stanza is declarative, and Homebrew now prefers casks for
# "download a binary and put it on PATH" distributions.
#
# `brew install dipto0321/tap/nodeup` keeps working after the switch — we
# verified the formula→cask transition against a scratch tap: Homebrew
# detects the change on `brew update`, installs the cask, unlinks the old
# keg, and tells the user to `brew uninstall --formula --force nodeup`.
# The tap repo carries a `tap_migrations.json` mapping so Homebrew can
# perform that hand-off automatically.
homebrew_casks:
- name: nodeup
repository:
owner: dipto0321
Expand All @@ -104,10 +138,8 @@ brews:
homepage: 'https://github.com/dipto0321/nodeup'
description: 'Automated Node.js version upgrade with global package migration'
license: 'MIT'
install: |
bin.install "nodeup"
test: |
system "#{bin}/nodeup", "--version"
binaries:
- nodeup

# Scoop bucket — auto-pushed on every release. Requires SCOOP_BUCKET_TOKEN.
scoops:
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal/cli/ cobra command wiring — thin layer, delegates to int
root.go NewRootCmd; registers all subcommands
upgrade.go nodeup upgrade (Phase 4, merged)
check.go / list.go / packages.go / config.go / version.go
cleanup_snapshots.go nodeup packages cleanup — delete snapshot / report files
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 viteplus.go nvm_windows.go
Expand All @@ -32,6 +33,8 @@ internal/packages/ npm global snapshot / restore / migrate (merged in PR
snapshot.go Snapshot(ctx, managerName, version) → ~/.../snapshots/<mgr>-<ver>.json
Restore(ctx, managerName, version) ([]PackageResult, error)
RestoreFromSnapshot(ctx, path) ([]PackageResult, error)
cleanup.go ListSnapshotFiles() / DeleteSnapshotFile() — file-level snapshot
enumeration + safe deletion (see #128)
report.go MigrationReport: per-package results, Save() → ~/.../reports/migration-<ts>.json
sentinel.go UpgradeSentinel: in-progress marker for interrupted-upgrade replay
internal/platform/
Expand All @@ -42,7 +45,7 @@ internal/ui/ single source of truth for user-facing output
theme.go DefaultTheme — lipgloss styles for FancyWriter
writer.go Writer interface + PlainWriter / FancyWriter implementations
spinner.go bubbletea-backed braille spinner for long-running steps
prompt.go huh-backed Confirm / Select helpers for interactive flows
prompt.go huh-backed Confirm / Select / MultiSelect helpers for interactive flows
```

## Key invariants — read before writing code
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,29 @@ nodeup version Print version info

Run `nodeup <command> --help` for the full flag reference.

### Reclaiming snapshot space

`nodeup upgrade` writes a snapshot of your global npm packages before it
migrates them, so it can roll back if something goes wrong. Once the
migration has replayed those packages onto the new Node version, the
snapshot has served its purpose — and nodeup deletes it automatically.

Snapshots that survive a *failed* migration are deliberate: they're the
rollback payload the interrupted-upgrade warning points at, and they stay
on disk until you deal with them. `nodeup packages cleanup` is how you
deal with them:

```bash
nodeup packages cleanup # pick files to delete (multi-select)
nodeup packages cleanup --list # show what would be deleted, delete nothing
nodeup packages cleanup --all --yes # delete every snapshot, no prompt
nodeup packages cleanup --reports # clean up migration reports instead
```

Pass `--keep-snapshots` to `nodeup upgrade` (or set
`packages.keep_snapshots: true` in the config) if you'd rather keep the
rollback payloads around for manual replays.

## How it works

In short:
Expand Down Expand Up @@ -229,6 +252,7 @@ packages:
skip:
- npm
- corepack
keep_snapshots: false # true = never auto-delete upgrade snapshots
cleanup:
auto: false
prompt: true
Expand All @@ -252,7 +276,7 @@ v1.1.0 → v1.2.1 history.
|---|---|---|
| 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 |
| 3 — Package snapshot/restore | ✅ | `nodeup packages {snapshot,restore,diff,cleanup}` + interrupted-upgrade sentinel + MigrationReport |
| 4 — Upgrade command | ✅ | End-to-end `nodeup upgrade` with snapshot → install → migrate → cleanup |
| 5 — Config subsystem | ✅ | YAML config at `~/.nodeup/config.yaml` + `config {show,get,set,init}` + env vars |
| 6 — Cross-platform polish | ✅ | `QuotePath`, interrupted-upgrade replay, system-node classifier, lock file |
Expand Down
15 changes: 13 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ manual steps, single static binary, no Node runtime dependency.

## §I Interfaces

- cmd: `nodeup upgrade [--lts|--current|--dry-run|--yes|--no-migrate|--no-cleanup|--cleanup|--cleanup-version <v>|--manager <name>]`
- cmd: `nodeup upgrade [--lts|--current|--dry-run|--yes|--no-migrate|--keep-snapshots|--no-cleanup|--cleanup|--cleanup-version <v>|--manager <name>]`
- cmd: `nodeup check [--json]`
- cmd: `nodeup list [--json]`
- cmd: `nodeup packages {snapshot|restore [--from <path>]|diff}`
- cmd: `nodeup packages {snapshot|restore [--from <path>]|diff|cleanup [--all|--reports|--list|--yes]}`
- cmd: `nodeup config {show|get <key>|set <key> <val>|init [--force]}`
- cmd: `nodeup version`
- api: `GET https://nodejs.org/dist/index.json` → `Manifest ([]ManifestVersion)`
Expand All @@ -53,6 +53,7 @@ manual steps, single static binary, no Node runtime dependency.
- file: `<DataDir>/nodeup.lock`
- env: `NODEUP_MANAGER`, `NODEUP_TRACK_LTS`, `NODEUP_TRACK_CURRENT`,
`NODEUP_PACKAGES_MIGRATE`, `NODEUP_PACKAGES_STRATEGY`,
`NODEUP_PACKAGES_KEEP_SNAPSHOTS`,
`NODEUP_CACHE_TTL`, `NODEUP_CONFIG`, `NODEUP_HOME`
- type: `Manager interface` → `Name() string`, `Detect() bool`,
`Version() (string, error)`, `ListInstalled() ([]semver.Version, error)`,
Expand Down Expand Up @@ -98,6 +99,15 @@ manual steps, single static binary, no Node runtime dependency.
- V21: lock file (`<DataDir>/nodeup.lock`) ! held around any critical
section that mutates installed Node versions or the config file
concurrently.
- V22: snapshot lifecycle — an upgrade's snapshots are deleted ⟺ the
migration replayed them successfully (or `--keep-snapshots` /
`packages.keep_snapshots` says otherwise). A snapshot whose restore
failed ⊥ deleted: it is the rollback payload the sentinel points at.
- V23: snapshot write ! atomic (temp file same dir + rename) — a
truncated body is invisible to every reader and would leak.
- V24: file deletion (snapshots, reports) ! confined to its own
directory — the target must resolve *inside* it after symlink
evaluation, and must be a regular file.

## §T Tasks

Expand All @@ -124,6 +134,7 @@ T19|x|fix double-prompt + sticky-no-override at cleanup decision point|#76,#95
T20|x|rest of cleanup/chore/ci/release batch|#62-#69,#89-#94,#99,#100,#107-#109
T21|x|doc drift: CLAUDE.md ui/lipgloss/phase-table stale; SPEC §T stale|#106
T22|x|add Vite+ (`vp`) as a supported version manager (full `vp env` surface + classifier + registry + docs)|I.managers,V14,#123,#124
T23|x|fix snapshot leak: auto-delete upgrade snapshots after a successful migration + `nodeup packages cleanup` CLI + atomic snapshot write|V22,V23,V24,#128

## §B Bugs

Expand Down
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ packages:
migrate: true
strategy: latest
skip: [yarn, pnpm]
keep_snapshots: false
cleanup:
auto: false
prompt: true
Expand Down Expand Up @@ -64,6 +65,7 @@ leaves the previous file untouched.
| `packages.migrate` | bool | `true` | Migrate global packages on upgrade |
| `packages.strategy` | string | `exact` | `exact` (pin version) or `latest` (re-fetch) |
| `packages.skip` | []string | `[npm, corepack, npx]` | Packages to never migrate |
| `packages.keep_snapshots` | bool | `false` | Keep the package snapshots an upgrade writes. By default nodeup deletes them once the migration has replayed them onto the new Node version. Set to `true` to keep the rollback payloads for manual replays. The `--keep-snapshots` flag on `nodeup upgrade` overrides this. |
| `cleanup.auto` | bool | `false` | Auto-remove old versions |
| `cleanup.prompt` | bool | `true` | Ask before removing each old version |
| `cache.ttl` | int | `3600` | Cache TTL in seconds |
Loading
Loading