Skip to content

feat(cli): implement end-to-end upgrade command#20

Merged
dipto0321 merged 4 commits into
mainfrom
feat/upgrade/end-to-end
Jun 30, 2026
Merged

feat(cli): implement end-to-end upgrade command#20
dipto0321 merged 4 commits into
mainfrom
feat/upgrade/end-to-end

Conversation

@dipto0321

@dipto0321 dipto0321 commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Closes out Phase 4: nodeup upgrade is now end-to-end instead of a stub. It detects the active Node version manager, resolves the latest LTS and/or Current from the nodejs.org index, computes an install plan (skipping anything already installed), snapshots the active Node version's global npm packages, installs the new versions, sets the newest one as the default, and restores the package snapshot onto the new versions. --dry-run prints the plan and exits without touching the disk.

This PR also fixes a latent JSON-union bug in internal/node/dist.go that the new upgrade flow exposed: the nodejs.org lts field is either the literal false (Current) or a codename string (LTS). The previous (LTS bool, TS string) pair silently dropped the codename whenever lts was JSON false, so LatestLTS/LatestCurrent could mis-classify rows. ManifestVersion is now (LTSCodename *string) with a custom UnmarshalJSON; tests cover both shapes.

Linked issues

Refs PR #20 review (Phase 4 — upgrade command). No GitHub issue numbers; this is the implementation branch for Phase 4 in the project status table.

Type of change

  • feat — new feature (MINOR bump)
  • fix — bug fix (PATCH bump)
  • refactor — no behavior change
  • perf — performance improvement
  • docs — documentation only
  • test — tests only
  • chore — maintenance / dependency bump
  • ci — CI/CD only
  • build — build system only
  • Breaking change (MAJOR bump) — explain below

Checklist

  • Title follows Conventional Commits (feat(scope): subject)
  • I ran make ci locally and it passes (tidy, fmt, vet, lint, test -race)
  • I added or updated tests for the change — TestManifestUnmarshalUnion covers both JSON shapes (false and codename string) and the existing TestLatestLTS / TestLatestCurrent / TestCacheExpiry were updated to the new struct.
  • I updated relevant docs (README, docs/, inline godoc) — CHANGELOG [Unreleased] notes both the new command and the LTSCodename field change. README's Quick Start already says nodeup upgrade, so no README edit was needed. Inline godoc on ManifestVersion, newUpgradeCmd, and the UnmarshalJSON method explains the JSON union.
  • No new linter warnings (golangci-lint v1.64.8, the version pinned in .github/workflows/ci.yml)
  • If breaking: I documented the migration path in the PR body and updated CHANGELOG.md — N/A

Scope notes / things reviewers may want to look at

  • Output routing. runUpgrade uses cmd.Printf / cmd.Println directly. CONTRIBUTING.md says user-facing output should flow through internal/ui, but internal/ui/ is currently an empty package per CLAUDE.md ("planned, not yet implemented"). check.go and packages.go also use cmd.Printf without complaint in already-merged PRs (docs: consolidate nodeup.md into README and remove the standalone design doc #9, docs: add status line to managers.md and docs index to README #11, feat(packages): implement snapshot and restore for global npm packages #19), so this PR is consistent with current practice. Happy to introduce internal/ui in a follow-up PR (Phase 4.5 / 5) and migrate all CLI commands in one pass.
  • contextcheck linter not yet enabled. golangci-lint does not currently include contextcheck, so cmd.Context() vs context.Background() is not enforced by lint. CLAUDE.md documents the rule, and the new commits follow it (fix(cli): honor cmd.Context() and surface parse errors in upgrade). Worth enabling the linter in a separate ci/lint PR so the rule is enforced project-wide.
  • LTS / TS fields removed. This is a breaking change for any external caller of node.ManifestVersion. Within this repo there are no external callers (the field is only consumed by LatestLTS / LatestCurrent, both updated). Worth a 0.1.0 MINOR bump per the SemVer rule, not a 1.0.0 MAJOR one, since internal/ packages are not Go-module-API stable yet.
  • --no-cleanup and --yes flags are declared but currently no-op. They are wired in runUpgrade with _ = noCleanup / _ = yes and TODO markers pointing at Phase 7 (cleanup) and "non-interactive mode". If you prefer, I can drop them from this PR and add them when the underlying logic lands, rather than ship silent flags. I left them in because the Long help text already advertises both behaviors and removing them would be a UX regression for anyone scripting around the (planned) interface.
  • No internal/cli/upgrade_test.go. No existing internal/cli/*.go has tests in this repo (check.go, list.go, packages.go, config.go, version.go all lack test files), so adding one here would be the first, and would need a non-trivial fake manager. Suggesting a follow-up test(cli): add upgrade_test.go with fake Manager PR.

Commits on this branch

9ec825d docs: note end-to-end upgrade command and LTS-field JSON fix
77b5b85 fix(cli): honor cmd.Context() and surface parse errors in upgrade
46ef9f3 feat(cli): implement end-to-end upgrade command   (the one the PR title tracks)

Per CONTRIBUTING.md, the PR will squash-merge with the source branch deleted, so the final merge commit subject will be feat(cli): implement end-to-end upgrade command (the first commit's subject).

Screenshots / output

nodeup upgrade --dry-run output shape:

Using manager: fnm
Dry run - would install:
  - 22.10.0
  - 24.0.0

nodeup upgrade (non-dry-run) output shape:

Using manager: fnm
Installing 22.10.0...
Installing 24.0.0...
Upgrade complete!

Per-version warnings on snapshot/restore failures go to stdout prefixed with Warning:.

@dipto0321 dipto0321 force-pushed the feat/upgrade/end-to-end branch 2 times, most recently from ddc0b39 to 0f82baa Compare June 30, 2026 04:34
@dipto0321

Copy link
Copy Markdown
Owner Author

Code review

Found 1 issue:

  1. ManifestVersion.LTS is typed as bool, but the nodejs.org API returns a union typefalse for Current releases and a string codename (e.g. "Iron") for LTS releases. json.Unmarshal cannot decode a JSON string into a bool field, so every LTS entry silently parses with LTS=false. The fallback TS string \json:"ts"`field (line 23) never helps because the JSON key islts, not ts. As a result LatestLTS()always returns "no LTS version found" andrunUpgradeexits with a fetch error on every real invocation. The bug was dormant whileupgrade.gowas a stub; this PR is the first to callFetchManifest()in production, activating it. Fix: usejson.RawMessageor a customUnmarshalJSONto handle thefalse | "codename"` union.

Date string `json:"date"` // e.g., "2024-07-01"
LTS bool `json:"lts"` // true if this is an LTS release
TS string `json:"ts"` // LTS codename like "Argon" or empty/nil
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Wires the full upgrade flow:
- detect -> resolve -> fetch versions -> diff
- snapshot global packages
- install new Node.js versions
- set default version
- restore packages

Supports --lts, --current, --dry-run, --no-migrate, --offline flags.

Also fixes internal/node/dist.go: ManifestVersion's `lts` field was typed
bool, but nodejs.org returns a JSON union (`false` for Current, a string
codename for LTS). The struct's json.Unmarshal rejected every LTS entry,
making FetchManifest fail in production. Rebuilt the field as
*LTSCodename with a custom UnmarshalJSON that handles both shapes, and
added a TestManifestUnmarshalUnion round-trip test against the real
index.json shape.

Closes #14
@dipto0321 dipto0321 force-pushed the feat/upgrade/end-to-end branch from 188b887 to 46ef9f3 Compare June 30, 2026 06:42
dipto0321 added 2 commits June 30, 2026 13:28
Per the project invariants recorded in CLAUDE.md, cobra RunE
implementations should use cmd.Context() rather than
context.Background() so the editor/Pty Host can cancel work
in-flight when the user aborts (e.g. an upgrade running in an
agent terminal that gets killed). Switches the two snapshot/restore
loops in runUpgrade over to cmd.Context().

Also stop swallowing the error from parseVersion when computing
the install plan. The previous `v, _ := parseVersion(...)` would
nil-deref the moment the manifest contained an unparseable row
(an LTS line missing a semver suffix, a stray pre-release tag, etc.).
Surface the error and abort the upgrade instead.
Adds two entries under [Unreleased] in CHANGELOG.md:
- Added: the now-end-to-end 'nodeup upgrade' command (detect →
  resolve → fetch → snapshot → install → migrate → cleanup) with
  --lts, --current, --dry-run, --no-migrate, --manager, --offline
  flags.
- Changed: ManifestVersion's (LTS bool, TS string) pair is replaced
  with a single LTSCodename *string plus a custom UnmarshalJSON so
  the nodejs.org 'lts' field union (false for Current, codename
  string for LTS) decodes cleanly. The old shape silently dropped
  the codename whenever 'lts' was the JSON literal false, which
  could route a Current release through LatestLTS().
@dipto0321 dipto0321 changed the title feat(upgrade): implement end-to-end upgrade command feat(cli): implement end-to-end upgrade command Jun 30, 2026
Adds a Claude Code editor helper at the repo root documenting the
project's architecture, build/test commands, code invariants (output
routing through internal/ui, cmd.Context() over context.Background(),
filepath.Join, platform.RunShell), commit/PR conventions, and the
current Phase status table.

Out of scope for the upgrade PR; added here for editor convenience.
The CHANGELOG note for the upgrade command still applies — this file
is purely editor-facing.
@dipto0321 dipto0321 force-pushed the feat/upgrade/end-to-end branch from 19df916 to def2ab0 Compare June 30, 2026 07:50
@cocogitto-bot

cocogitto-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

✔️ 46ef9f3...def2ab0 - Conventional commits check succeeded.

@dipto0321 dipto0321 merged commit a46ebb4 into main Jun 30, 2026
10 checks passed
@dipto0321 dipto0321 deleted the feat/upgrade/end-to-end branch June 30, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant