// This file implements the update operation (#164): bring an installed package // up to the latest version published on npm. Update builds on the pieces the // earlier issues provided — the lockfile (#154) records what is installed and // from which source, Fetch (#156) pulls the latest tarball, and the shared // installFetched path (#162) re-classifies and re-distributes. // // The flow per package is: look up its recorded source, fetch the latest // version, and compare against the installed version. If unchanged, it is a // no-op ("up to date"). Otherwise the old files are removed and the freshly // fetched version is distributed and recorded. Fetch/classify happen before any // removal, so a fetch failure leaves the existing install untouched. package pkgmgr import ( "fmt" "io" ) // UpdateResult reports what Update did for one package. type UpdateResult struct { Name string // OldVersion is the version that was installed before the update. OldVersion string // NewVersion is the version after the update (== OldVersion when no-op). NewVersion string // Updated is true when a newer version was fetched and installed. Updated bool } // Update brings the single installed package name up to the latest version from // its recorded source. Progress is written to logw when non-nil. Updating a // package that is not installed is an error. When the installed version is // already latest, it is a no-op and Updated is false. // // The latest tarball is fetched and classified before the old files are // removed, so a fetch or classify failure leaves the prior install intact. func Update(name, lockfilePath string, logw io.Writer) (UpdateResult, error) { logf := func(format string, a ...any) { if logw != nil { fmt.Fprintf(logw, format, a...) } } lf, err := Load(lockfilePath) if err != nil { return UpdateResult{}, err } p, ok := lf.Get(name) if !ok { return UpdateResult{}, fmt.Errorf("package not installed: %s", name) } // Resolve the source to its latest version by dropping any pinned version, // so `update` always targets the newest published release. ref, err := ParsePackageRef(p.Source) if err != nil { return UpdateResult{}, fmt.Errorf("pkgmgr: bad recorded source for %s: %w", name, err) } ref.Version = "" logf("Fetching %s ...\n", ref.String()) fetched, err := Fetch(ref) if err != nil { return UpdateResult{}, err // old install untouched } defer fetched.Cleanup() _, newVersion, _, err := Classify(fetched.Dir) if err != nil { return UpdateResult{}, err // old install untouched } if newVersion == p.Version { logf("%s is up to date\n", name) return UpdateResult{Name: name, OldVersion: p.Version, NewVersion: p.Version, Updated: false}, nil } // Newer version fetched: remove the old files, then distribute the new one. if err := Uninstall(name, lockfilePath, logw); err != nil { return UpdateResult{}, err } if _, err := installFetched(fetched.Dir, ref, lockfilePath, logf); err != nil { return UpdateResult{}, err } logf("Updated %s %s -> %s\n", name, p.Version, newVersion) return UpdateResult{Name: name, OldVersion: p.Version, NewVersion: newVersion, Updated: true}, nil }