diff --git a/internal/update/install_method.go b/internal/update/install_method.go index a3d279ff..f882411e 100644 --- a/internal/update/install_method.go +++ b/internal/update/install_method.go @@ -64,26 +64,3 @@ func classifyPath(resolved string) InstallMethod { return InstallBinary } - -// npmProjectDir returns the project directory for a local npm install, -// or empty string for a global install. A local install has a package.json -// in the parent of the node_modules directory. -func npmProjectDir(resolvedPath string) string { - // Walk up to find node_modules, then check for package.json one level above - dir := resolvedPath - for { - parent := filepath.Dir(dir) - if parent == dir { - break - } - if filepath.Base(dir) == "node_modules" { - pkgJSON := filepath.Join(parent, "package.json") - if _, err := os.Stat(pkgJSON); err == nil { - return parent - } - return "" - } - dir = parent - } - return "" -} diff --git a/internal/update/install_method_test.go b/internal/update/install_method_test.go index 2ac64e38..ca2087eb 100644 --- a/internal/update/install_method_test.go +++ b/internal/update/install_method_test.go @@ -1,8 +1,6 @@ package update import ( - "os" - "path/filepath" "testing" ) @@ -34,6 +32,11 @@ func TestClassifyPath(t *testing.T) { path: "/usr/local/lib/node_modules/@localstack/lstk_darwin_amd64/lstk", want: InstallNPM, }, + { + name: "npm global install via asdf", + path: "/Users/geo/.asdf/installs/nodejs/22.12.0/lib/node_modules/@localstack/lstk_darwin_arm64/lstk", + want: InstallNPM, + }, { name: "standalone binary in usr local bin", path: "/usr/local/bin/lstk", @@ -61,46 +64,3 @@ func TestClassifyPath(t *testing.T) { }) } } - -func TestNpmProjectDir(t *testing.T) { - t.Parallel() - - t.Run("local install with package.json", func(t *testing.T) { - t.Parallel() - dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}"), 0o644); err != nil { - t.Fatal(err) - } - binaryPath := filepath.Join(dir, "node_modules", "@localstack", "lstk_darwin_arm64", "lstk") - if err := os.MkdirAll(filepath.Dir(binaryPath), 0o755); err != nil { - t.Fatal(err) - } - - got := npmProjectDir(binaryPath) - if got != dir { - t.Fatalf("npmProjectDir() = %q, want %q", got, dir) - } - }) - - t.Run("global install without package.json", func(t *testing.T) { - t.Parallel() - dir := t.TempDir() - binaryPath := filepath.Join(dir, "lib", "node_modules", "@localstack", "lstk_darwin_arm64", "lstk") - if err := os.MkdirAll(filepath.Dir(binaryPath), 0o755); err != nil { - t.Fatal(err) - } - - got := npmProjectDir(binaryPath) - if got != "" { - t.Fatalf("npmProjectDir() = %q, want empty string", got) - } - }) - - t.Run("non-npm path", func(t *testing.T) { - t.Parallel() - got := npmProjectDir("/usr/local/bin/lstk") - if got != "" { - t.Fatalf("npmProjectDir() = %q, want empty string", got) - } - }) -} diff --git a/internal/update/npm.go b/internal/update/npm.go index 0ffab1e8..3346421b 100644 --- a/internal/update/npm.go +++ b/internal/update/npm.go @@ -7,14 +7,8 @@ import ( "github.com/localstack/lstk/internal/output" ) -func updateNPM(ctx context.Context, sink output.Sink, projectDir string) error { - var cmd *exec.Cmd - if projectDir != "" { - cmd = exec.CommandContext(ctx, "npm", "install", "@localstack/lstk@latest") - cmd.Dir = projectDir - } else { - cmd = exec.CommandContext(ctx, "npm", "install", "-g", "@localstack/lstk@latest") - } +func updateNPM(ctx context.Context, sink output.Sink) error { + cmd := exec.CommandContext(ctx, "npm", "install", "-g", "@localstack/lstk@latest") w := newLogLineWriter(sink, output.LogSourceNPM) cmd.Stdout = w cmd.Stderr = w diff --git a/internal/update/update.go b/internal/update/update.go index a59d6973..f1b35fb9 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -63,13 +63,8 @@ func applyUpdate(ctx context.Context, sink output.Sink, latest, githubToken stri output.EmitNote(sink, "Installed through Homebrew, running brew upgrade") err = updateHomebrew(ctx, sink) case InstallNPM: - projectDir := npmProjectDir(info.ResolvedPath) - if projectDir != "" { - output.EmitNote(sink, fmt.Sprintf("Installed through npm (local), running npm install in %s", projectDir)) - } else { - output.EmitNote(sink, "Installed through npm (global), running npm install -g") - } - err = updateNPM(ctx, sink, projectDir) + output.EmitNote(sink, "Installed through npm, running npm install -g") + err = updateNPM(ctx, sink) default: output.EmitSpinnerStart(sink, "Downloading update") err = updateBinary(ctx, latest, githubToken) diff --git a/test/integration/update_test.go b/test/integration/update_test.go index 467f2c29..c5da4890 100644 --- a/test/integration/update_test.go +++ b/test/integration/update_test.go @@ -47,12 +47,19 @@ func requireNPM(t *testing.T) { } } -func TestUpdateNPMLocalInstall(t *testing.T) { +func TestUpdateNPMInstall(t *testing.T) { requireNPM(t) + // Skip if lstk is already installed globally (e.g., via Homebrew). + // npm install -g fails with EEXIST when it tries to create a symlink + // over an existing binary at the same path. + if path, err := exec.LookPath("lstk"); err == nil { + t.Skipf("lstk already installed at %s, would conflict with npm install -g", path) + } + ctx := testContext(t) - // Set up a fake local npm project. + // Set up a fake local npm project so we get a binary inside node_modules. // On Windows, t.TempDir() may return a short 8.3 path (e.g. RUNNER~1) // while the program resolves the long path. EvalSymlinks normalizes both. projectDir, err := filepath.EvalSymlinks(t.TempDir()) @@ -93,7 +100,8 @@ func TestUpdateNPMLocalInstall(t *testing.T) { out, err = buildCmd.CombinedOutput() require.NoError(t, err, "go build failed: %s", string(out)) - // Run the binary directly (not through npx) so os.Executable() resolves to the node_modules path + // Run the binary directly (not through npx) so os.Executable() resolves to the node_modules path. + // The update should always use `npm install -g` regardless of local/global context. cmd := exec.CommandContext(ctx, nmBinaryPath, "update", "--non-interactive") cmd.Dir = projectDir stdout, err := cmd.CombinedOutput() @@ -101,8 +109,7 @@ func TestUpdateNPMLocalInstall(t *testing.T) { require.NoError(t, err, "lstk update failed: %s", stdoutStr) requireExitCode(t, 0, err) - assert.Contains(t, stdoutStr, "npm (local)", "should detect local npm install") - assert.Contains(t, stdoutStr, projectDir, "should show the project directory") + assert.Contains(t, stdoutStr, "npm install -g", "should always use global install") assert.Contains(t, stdoutStr, "Updated to", "should complete the update") }