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
47 changes: 25 additions & 22 deletions internal/cli/check.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
"encoding/json"
"fmt"
"strings"
Expand All @@ -10,6 +11,7 @@ import (

"github.com/dipto0321/nodeup/internal/detector"
"github.com/dipto0321/nodeup/internal/node"
"github.com/dipto0321/nodeup/internal/ui"
)

// systemNodeJSON describes the on-disk `node` binary found on PATH,
Expand Down Expand Up @@ -101,14 +103,15 @@ func runCheck(cmd *cobra.Command, args []string) error {
}
}

w := writerFromCmd(cmd)
if asJSON {
return outputCheckJSON(cmd, lts, current, installed, sysNode)
return outputCheckJSON(w, cmd.Context(), lts, current, installed, sysNode)
}

return outputCheckTable(cmd, lts, current, installed, sysNode)
return outputCheckTable(w, cmd.Context(), lts, current, installed, sysNode)
}

func outputCheckJSON(cmd *cobra.Command, lts, current *node.ManifestVersion, installed detector.Registry, sysNode *systemNodeJSON) error {
func outputCheckJSON(w ui.Writer, ctx context.Context, lts, current *node.ManifestVersion, installed detector.Registry, sysNode *systemNodeJSON) error {
type checkOutput struct {
LTS *node.ManifestVersion `json:"lts"`
Current *node.ManifestVersion `json:"current"`
Expand All @@ -117,8 +120,8 @@ func outputCheckJSON(cmd *cobra.Command, lts, current *node.ManifestVersion, ins
}

installedVersions := make([]string, 0)
for _, m := range installed.Found {
versions, err := m.ListInstalled(cmd.Context())
for _, mgr := range installed.Found {
versions, err := mgr.ListInstalled(ctx)
if err != nil {
continue
}
Expand All @@ -138,46 +141,46 @@ func outputCheckJSON(cmd *cobra.Command, lts, current *node.ManifestVersion, ins
if err != nil {
return err
}
cmd.Println(string(data))
w.Println(string(data))
return nil
}

func outputCheckTable(cmd *cobra.Command, lts, current *node.ManifestVersion, installed detector.Registry, sysNode *systemNodeJSON) error {
cmd.Println()
cmd.Printf(" LTS: %s (released %s)\n", lts.Version, lts.Date)
cmd.Printf(" Current: %s (released %s)\n", current.Version, current.Date)
cmd.Println()
func outputCheckTable(w ui.Writer, ctx context.Context, lts, current *node.ManifestVersion, installed detector.Registry, sysNode *systemNodeJSON) error {
w.Println("")
w.Info(fmt.Sprintf(" LTS: %s (released %s)", lts.Version, lts.Date))
w.Info(fmt.Sprintf(" Current: %s (released %s)", current.Version, current.Date))
w.Println("")

if len(installed.Found) == 0 {
cmd.Println("No Node.js version manager detected.")
w.Info("No Node.js version manager detected.")
} else {
cmd.Println("Installed versions:")
for _, m := range installed.Found {
versions, err := m.ListInstalled(cmd.Context())
w.Println("Installed versions:")
for _, mgr := range installed.Found {
versions, err := mgr.ListInstalled(ctx)
if err != nil {
cmd.Printf(" - %s: [error listing versions]\n", m.Name())
w.Warn(fmt.Sprintf(" - %s: [error listing versions]", mgr.Name()))
continue
}
cmd.Printf(" - %s: %s\n", m.Name(), formatVersions(versions))
w.Info(fmt.Sprintf(" - %s: %s", mgr.Name(), formatVersions(versions)))
}
}

// Surface the on-PATH `node` classification. When sysNode is nil
// the probe didn't run (or `which node` failed) — we say so
// explicitly rather than staying silent, so the user has a
// single source of truth for what nodeup sees.
cmd.Println()
w.Println("")
if sysNode == nil {
cmd.Println("System node: (could not probe `node` on PATH)")
w.Info("System node: (could not probe `node` on PATH)")
return nil
}
switch sysNode.Kind {
case "manager":
cmd.Printf("System node: %s (managed by %s)\n", sysNode.Path, sysNode.Manager)
w.Info(fmt.Sprintf("System node: %s (managed by %s)", sysNode.Path, sysNode.Manager))
case "unknown":
// Path matched no known layout. Don't print a long warning
// here — `nodeup upgrade` is where the warning belongs.
cmd.Printf("System node: %s (unrecognized layout)\n", sysNode.Path)
w.Info(fmt.Sprintf("System node: %s (unrecognized layout)", sysNode.Path))
default:
// OS-package / snap / flatpak / homebrew-core. Render the
// same warning text that `nodeup upgrade` would print, so
Expand All @@ -193,7 +196,7 @@ func outputCheckTable(cmd *cobra.Command, lts, current *node.ManifestVersion, in
// Indent each rendered line by two spaces so it lines up
// with the rest of the table block.
for _, line := range strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") {
cmd.Printf(" %s\n", line)
w.Info(" " + line)
}
}

Expand Down
13 changes: 7 additions & 6 deletions internal/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ to a file as a starting point for a new config.`,
return fmt.Errorf("marshal config: %w", err)
}
path, _ := config.DefaultPath()
fmt.Fprintf(cmd.OutOrStdout(), "# effective config (source: %s)\n%s", path, out)
w := writerFromCmd(cmd)
w.Println(fmt.Sprintf("# effective config (source: %s)\n%s", path, out))
return nil
})
},
Expand All @@ -95,7 +96,7 @@ func newConfigGetCmd() *cobra.Command {
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), v)
writerFromCmd(cmd).Println(v)
return nil
})
},
Expand Down Expand Up @@ -137,7 +138,7 @@ func newConfigSetCmd() *cobra.Command {
}
defer func() {
if rerr := configLock.Release(); rerr != nil {
fmt.Fprintf(cmd.OutOrStdout(), "Warning: failed to release config lock: %v\n", rerr)
writerFromCmd(cmd).Warn(fmt.Sprintf("Warning: failed to release config lock: %v", rerr))
}
}()

Expand Down Expand Up @@ -170,7 +171,7 @@ func newConfigSetCmd() *cobra.Command {
if err := config.Save(path, current); err != nil {
return fmt.Errorf("save config to %s: %w", path, err)
}
fmt.Fprintf(cmd.OutOrStdout(), "set %s=%s (saved to %s)\n", args[0], args[1], path)
writerFromCmd(cmd).Success(fmt.Sprintf("set %s=%s (saved to %s)", args[0], args[1], path))
return nil
},
}
Expand Down Expand Up @@ -204,7 +205,7 @@ to overwrite an existing file unless --force is passed.`,
}
defer func() {
if rerr := configLock.Release(); rerr != nil {
fmt.Fprintf(cmd.OutOrStdout(), "Warning: failed to release config lock: %v\n", rerr)
writerFromCmd(cmd).Warn(fmt.Sprintf("Warning: failed to release config lock: %v", rerr))
}
}()

Expand All @@ -217,7 +218,7 @@ to overwrite an existing file unless --force is passed.`,
if err := config.Save(path, cfg); err != nil {
return fmt.Errorf("save config to %s: %w", path, err)
}
fmt.Fprintf(cmd.OutOrStdout(), "wrote scaffolded config to %s\n", path)
writerFromCmd(cmd).Success(fmt.Sprintf("wrote scaffolded config to %s", path))
return nil
},
}
Expand Down
24 changes: 13 additions & 11 deletions internal/cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/spf13/cobra"

"github.com/dipto0321/nodeup/internal/detector"
"github.com/dipto0321/nodeup/internal/ui"
)

// installedEntryJSON is a single manager's contributions to the
Expand Down Expand Up @@ -122,38 +123,39 @@ func runList(cmd *cobra.Command, args []string) error {
}
}

w := writerFromCmd(cmd)
if asJSON {
return outputListJSON(cmd, listOutputJSON{Installed: entries, Current: active})
return outputListJSON(w, listOutputJSON{Installed: entries, Current: active})
}
return outputListTable(cmd, entries)
return outputListTable(w, entries)
}

func outputListJSON(cmd *cobra.Command, out listOutputJSON) error {
func outputListJSON(w ui.Writer, out listOutputJSON) error {
data, err := json.MarshalIndent(out, "", " ")
if err != nil {
return err
}
cmd.Println(string(data))
w.Println(string(data))
return nil
}

func outputListTable(cmd *cobra.Command, entries []installedEntryJSON) error {
cmd.Println()
func outputListTable(w ui.Writer, entries []installedEntryJSON) error {
w.Println("")
if len(entries) == 0 {
cmd.Println("No Node.js version manager detected.")
cmd.Println("Install one of: fnm, nvm, Volta, asdf, mise, n, nodenv, nvm-windows.")
w.Info("No Node.js version manager detected.")
w.Info("Install one of: fnm, nvm, Volta, asdf, mise, n, nodenv, nvm-windows.")
return nil
}
for _, e := range entries {
if e.Error != "" {
cmd.Printf(" %s: [error listing versions: %s]\n", e.Manager, e.Error)
w.Warn(fmt.Sprintf(" %s: [error listing versions: %s]", e.Manager, e.Error))
continue
}
if len(e.Versions) == 0 {
cmd.Printf(" %s: (no versions installed)\n", e.Manager)
w.Info(fmt.Sprintf(" %s: (no versions installed)", e.Manager))
continue
}
cmd.Printf(" %s: %s\n", e.Manager, formatListVersions(e.Versions))
w.Info(fmt.Sprintf(" %s: %s", e.Manager, formatListVersions(e.Versions)))
}
return nil
}
Expand Down
18 changes: 11 additions & 7 deletions internal/cli/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import (
)

// newBufCmd returns a *cobra.Command with its output redirected to an
// in-memory buffer. Cobra's Println et al. write to OutOrStdout(); by
// overriding it we capture output without touching the real terminal.
// in-memory buffer. Cobra's Println et al. write to OutOrStdout();
// Warn/Error route to ErrOrStderr. We redirect BOTH to the same
// buffer so test assertions can verify everything in one place —
// this matters now that the cli migrated to ui.Writer, which
// routes warn/error to ErrOrStderr.
//
// We construct a bare Cobra command here rather than going through
// NewRootCmd so the list tests stay independent of the full command
Expand All @@ -25,6 +28,7 @@ func newBufCmd(t *testing.T) (*cobra.Command, *bytes.Buffer) {
var buf bytes.Buffer
c := &cobra.Command{Use: "test"}
c.SetOut(&buf)
c.SetErr(&buf)
return c, &buf
}

Expand Down Expand Up @@ -190,7 +194,7 @@ func TestRegistryNames_OrderingPreserved(t *testing.T) {
func TestOutputListJSON_HappyPath(t *testing.T) {
cmd, buf := newBufCmd(t)
cur := "20.18.0"
if err := outputListJSON(cmd, listOutputJSON{
if err := outputListJSON(writerFromCmd(cmd), listOutputJSON{
Installed: []installedEntryJSON{
{Manager: "fnm", Versions: []string{"18.20.4", "20.18.0", "22.11.0"}},
{Manager: "volta", Versions: []string{"20.11.0"}},
Expand Down Expand Up @@ -220,7 +224,7 @@ func TestOutputListJSON_HappyPath(t *testing.T) {

func TestOutputListJSON_OmitEmptyCurrent(t *testing.T) {
cmd, buf := newBufCmd(t)
if err := outputListJSON(cmd, listOutputJSON{
if err := outputListJSON(writerFromCmd(cmd), listOutputJSON{
Installed: []installedEntryJSON{{Manager: "fnm"}},
}); err != nil {
t.Fatalf("outputListJSON: %v", err)
Expand All @@ -233,7 +237,7 @@ func TestOutputListJSON_OmitEmptyCurrent(t *testing.T) {

func TestOutputListJSON_OmitEmptyError(t *testing.T) {
cmd, buf := newBufCmd(t)
if err := outputListJSON(cmd, listOutputJSON{
if err := outputListJSON(writerFromCmd(cmd), listOutputJSON{
Installed: []installedEntryJSON{{Manager: "fnm", Versions: []string{"20.0.0"}}},
}); err != nil {
t.Fatalf("outputListJSON: %v", err)
Expand All @@ -247,7 +251,7 @@ func TestOutputListJSON_OmitEmptyError(t *testing.T) {

func TestOutputListTable_Empty(t *testing.T) {
cmd, buf := newBufCmd(t)
if err := outputListTable(cmd, nil); err != nil {
if err := outputListTable(writerFromCmd(cmd), nil); err != nil {
t.Fatalf("outputListTable: %v", err)
}
out := buf.String()
Expand All @@ -267,7 +271,7 @@ func TestOutputListTable_MixedStates(t *testing.T) {
{Manager: "volta"}, // neither versions nor error — falls through with "(no versions installed)"
{Manager: "asdf", Error: "boom"},
}
if err := outputListTable(cmd, entries); err != nil {
if err := outputListTable(writerFromCmd(cmd), entries); err != nil {
t.Fatalf("outputListTable: %v", err)
}
out := buf.String()
Expand Down
26 changes: 13 additions & 13 deletions internal/cli/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func runSnapshot(cmd *cobra.Command, args []string) error {
return fmt.Errorf("snapshot failed: %w", err)
}

cmd.Printf("Snapshot saved for %s %s\n", m.Name(), version)
writerFromCmd(cmd).Success(fmt.Sprintf("Snapshot saved for %s %s", m.Name(), version))
return nil
}

Expand Down Expand Up @@ -87,14 +87,14 @@ func runPackagesList(cmd *cobra.Command, args []string) error {
}

if len(snapshots) == 0 {
cmd.Println("No snapshots found.")
writerFromCmd(cmd).Info("No snapshots found.")
return nil
}

for _, s := range snapshots {
cmd.Printf("\n%s (Node %s):\n", s.Manager, s.NodeVersion)
writerFromCmd(cmd).Info(fmt.Sprintf("%s (Node %s):", s.Manager, s.NodeVersion))
for _, p := range s.Packages {
cmd.Printf(" - %s@%s\n", p.Name, p.Version)
writerFromCmd(cmd).Info(fmt.Sprintf(" - %s@%s", p.Name, p.Version))
}
}
return nil
Expand Down Expand Up @@ -156,7 +156,7 @@ func runRestore(cmd *cobra.Command, args []string) error {
// packages — has already been achieved.
clearSentinel := func() {
if err := packages.RemoveSentinel(); err != nil {
cmd.Printf("Warning: failed to clear upgrade sentinel: %v\n", err)
writerFromCmd(cmd).Warn(fmt.Sprintf("Warning: failed to clear upgrade sentinel: %v", err))
}
}

Expand Down Expand Up @@ -185,11 +185,11 @@ func runRestore(cmd *cobra.Command, args []string) error {
report.AddResult(r)
}
if err := report.Save(); err != nil {
cmd.Printf("Warning: failed to write migration report: %v\n", err)
writerFromCmd(cmd).Warn(fmt.Sprintf("Warning: failed to write migration report: %v", err))
return
}
if p, perr := report.Path(); perr == nil {
cmd.Printf("Migration report: %s\n", p)
writerFromCmd(cmd).Info(fmt.Sprintf("Migration report: %s", p))
}
}

Expand All @@ -207,7 +207,7 @@ func runRestore(cmd *cobra.Command, args []string) error {
}
writeReport(outcome.Snapshot, "", "", outcome.Results)
clearSentinel()
cmd.Printf("Restored packages from %s\n", fromPath)
writerFromCmd(cmd).Success(fmt.Sprintf("Restored packages from %s", fromPath))
return nil
}

Expand Down Expand Up @@ -243,7 +243,7 @@ func runRestore(cmd *cobra.Command, args []string) error {
writeReport(outcome.Snapshot, managerName, versionStr, outcome.Results)
clearSentinel()

cmd.Printf("Restored packages for %s %s\n", managerName, versionStr)
writerFromCmd(cmd).Success(fmt.Sprintf("Restored packages for %s %s", managerName, versionStr))
return nil
}

Expand Down Expand Up @@ -277,14 +277,14 @@ func runDiff(cmd *cobra.Command, args []string) error {

added, removed := packages.DiffSnapshots(s1.Packages, s2.Packages)

cmd.Printf("Added packages:\n")
writerFromCmd(cmd).Println("Added packages:")
for _, p := range added {
cmd.Printf(" + %s@%s\n", p.Name, p.Version)
writerFromCmd(cmd).Info(fmt.Sprintf(" + %s@%s", p.Name, p.Version))
}

cmd.Printf("Removed packages:\n")
writerFromCmd(cmd).Println("Removed packages:")
for _, p := range removed {
cmd.Printf(" - %s@%s\n", p.Name, p.Version)
writerFromCmd(cmd).Info(fmt.Sprintf(" - %s@%s", p.Name, p.Version))
}

return nil
Expand Down
Loading
Loading