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
23 changes: 0 additions & 23 deletions internal/update/install_method.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}
50 changes: 5 additions & 45 deletions internal/update/install_method_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package update

import (
"os"
"path/filepath"
"testing"
)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
}
})
}
10 changes: 2 additions & 8 deletions internal/update/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 2 additions & 7 deletions internal/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 12 additions & 5 deletions test/integration/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -93,16 +100,16 @@ 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()
stdoutStr := string(stdout)

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")
}

Expand Down