Skip to content

Commit f4fdc8a

Browse files
rgarciacursoragent
andauthored
fix: detect npm global installs in update upgrade suggestion (#120)
## Summary - Fix update check suggesting `bun add -g` for users who installed via `npm i -g` with a custom prefix (e.g. `~/.npm-global/`). - The npm path matcher now also checks for `/.npm-global/` and `/.npm/` in the binary path, so it correctly identifies the install method before falling through to env-based detection (which was matching `BUN_INSTALL`). - Refactored path-matching helpers into named package-level functions and added tests. ## Test plan - [x] `go test ./pkg/update/` passes - [x] `go vet ./pkg/update/` clean - [ ] Manual: install via `npm i -g @onkernel/cli` with a custom npm prefix and verify the upgrade suggestion says `npm i -g @onkernel/cli@latest` Made with [Cursor](https://cursor.com) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Scoped to upgrade-suggestion heuristics and adds tests; minimal impact beyond how the CLI chooses the displayed upgrade command. > > **Overview** > Fixes install-method detection so `npm` global installs (including custom prefixes like `~/.npm-global` and `~/.npm`) are identified via binary path matching, avoiding incorrect fallback to `bun` env-based detection and ensuring the upgrade banner suggests `npm i -g @onkernel/cli@latest`. > > Refactors detection into reusable helpers (`installMethodRules`, `pathMatches*`) and extracts `suggestUpgradeCommandForMethod` for testability, with new unit tests covering path matchers, rule precedence, and upgrade-command output. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 280e56f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4c05c17 commit f4fdc8a

File tree

2 files changed

+156
-29
lines changed

2 files changed

+156
-29
lines changed

pkg/update/check.go

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,45 @@ const (
290290
InstallMethodUnknown InstallMethod = "unknown"
291291
)
292292

293+
type installRule struct {
294+
check func(string) bool
295+
envKeys []string
296+
method InstallMethod
297+
}
298+
299+
func normalizePath(p string) string { return strings.ToLower(filepath.ToSlash(p)) }
300+
301+
func pathMatchesHomebrew(p string) bool {
302+
p = normalizePath(p)
303+
return strings.Contains(p, "homebrew") || strings.Contains(p, "/cellar/")
304+
}
305+
306+
func pathMatchesBun(p string) bool {
307+
return strings.Contains(normalizePath(p), "/.bun/")
308+
}
309+
310+
func pathMatchesPNPM(p string) bool {
311+
p = normalizePath(p)
312+
return strings.Contains(p, "/pnpm/") || strings.Contains(p, "/.pnpm/")
313+
}
314+
315+
func pathMatchesNPM(p string) bool {
316+
p = normalizePath(p)
317+
return strings.Contains(p, "/npm/") ||
318+
strings.Contains(p, "/node_modules/.bin/") ||
319+
strings.Contains(p, "/.npm-global/") ||
320+
strings.Contains(p, "/.npm/")
321+
}
322+
323+
func installMethodRules() []installRule {
324+
return []installRule{
325+
{pathMatchesHomebrew, nil, InstallMethodBrew},
326+
{pathMatchesBun, []string{"BUN_INSTALL"}, InstallMethodBun},
327+
{pathMatchesPNPM, []string{"PNPM_HOME"}, InstallMethodPNPM},
328+
{pathMatchesNPM, []string{"NPM_CONFIG_PREFIX", "npm_config_prefix", "VOLTA_HOME"}, InstallMethodNPM},
329+
}
330+
}
331+
293332
// DetectInstallMethod detects how kernel was installed and returns the method
294333
// along with the path to the kernel binary.
295334
func DetectInstallMethod() (InstallMethod, string) {
@@ -311,34 +350,7 @@ func DetectInstallMethod() (InstallMethod, string) {
311350
}
312351
}
313352

314-
// Helpers
315-
norm := func(p string) string { return strings.ToLower(filepath.ToSlash(p)) }
316-
hasHomebrew := func(p string) bool {
317-
p = norm(p)
318-
return strings.Contains(p, "homebrew") || strings.Contains(p, "/cellar/")
319-
}
320-
hasBun := func(p string) bool { p = norm(p); return strings.Contains(p, "/.bun/") }
321-
hasPNPM := func(p string) bool {
322-
p = norm(p)
323-
return strings.Contains(p, "/pnpm/") || strings.Contains(p, "/.pnpm/")
324-
}
325-
hasNPM := func(p string) bool {
326-
p = norm(p)
327-
return strings.Contains(p, "/npm/") || strings.Contains(p, "/node_modules/.bin/")
328-
}
329-
330-
type rule struct {
331-
check func(string) bool
332-
envKeys []string
333-
method InstallMethod
334-
}
335-
336-
rules := []rule{
337-
{hasHomebrew, nil, InstallMethodBrew},
338-
{hasBun, []string{"BUN_INSTALL"}, InstallMethodBun},
339-
{hasPNPM, []string{"PNPM_HOME"}, InstallMethodPNPM},
340-
{hasNPM, []string{"NPM_CONFIG_PREFIX", "npm_config_prefix", "VOLTA_HOME"}, InstallMethodNPM},
341-
}
353+
rules := installMethodRules()
342354

343355
// Path-based detection first
344356
for _, c := range candidates {
@@ -374,7 +386,10 @@ func DetectInstallMethod() (InstallMethod, string) {
374386
// returns a tailored upgrade command. Falls back to default brew command on unknown.
375387
func SuggestUpgradeCommand() string {
376388
method, _ := DetectInstallMethod()
389+
return suggestUpgradeCommandForMethod(method)
390+
}
377391

392+
func suggestUpgradeCommandForMethod(method InstallMethod) string {
378393
switch method {
379394
case InstallMethodBrew:
380395
return "brew upgrade kernel/tap/kernel"
@@ -385,7 +400,6 @@ func SuggestUpgradeCommand() string {
385400
case InstallMethodBun:
386401
return "bun add -g @onkernel/cli@latest"
387402
default:
388-
// Default suggestion when unknown
389403
return "brew upgrade kernel/tap/kernel"
390404
}
391405
}

pkg/update/check_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package update
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestSuggestUpgradeCommandForMethod(t *testing.T) {
10+
tests := []struct {
11+
method InstallMethod
12+
expected string
13+
}{
14+
{InstallMethodBrew, "brew upgrade kernel/tap/kernel"},
15+
{InstallMethodNPM, "npm i -g @onkernel/cli@latest"},
16+
{InstallMethodPNPM, "pnpm add -g @onkernel/cli@latest"},
17+
{InstallMethodBun, "bun add -g @onkernel/cli@latest"},
18+
{InstallMethodUnknown, "brew upgrade kernel/tap/kernel"},
19+
}
20+
for _, tt := range tests {
21+
t.Run(string(tt.method), func(t *testing.T) {
22+
assert.Equal(t, tt.expected, suggestUpgradeCommandForMethod(tt.method))
23+
})
24+
}
25+
}
26+
27+
func TestPathMatchesNPM(t *testing.T) {
28+
tests := []struct {
29+
path string
30+
expected bool
31+
}{
32+
{"/home/user/.npm-global/bin/kernel", true},
33+
{"/home/user/.npm/bin/kernel", true},
34+
{"/usr/local/lib/node_modules/.bin/kernel", true},
35+
{"/home/user/.local/share/npm/bin/kernel", true},
36+
{"/opt/homebrew/bin/kernel", false},
37+
{"/home/user/.bun/bin/kernel", false},
38+
{"/home/user/.local/share/pnpm/kernel", false},
39+
}
40+
for _, tt := range tests {
41+
t.Run(tt.path, func(t *testing.T) {
42+
assert.Equal(t, tt.expected, pathMatchesNPM(tt.path))
43+
})
44+
}
45+
}
46+
47+
func TestPathMatchesBun(t *testing.T) {
48+
tests := []struct {
49+
path string
50+
expected bool
51+
}{
52+
{"/home/user/.bun/bin/kernel", true},
53+
{"/home/user/.npm-global/bin/kernel", false},
54+
{"/opt/homebrew/bin/kernel", false},
55+
}
56+
for _, tt := range tests {
57+
t.Run(tt.path, func(t *testing.T) {
58+
assert.Equal(t, tt.expected, pathMatchesBun(tt.path))
59+
})
60+
}
61+
}
62+
63+
func TestPathMatchesPNPM(t *testing.T) {
64+
tests := []struct {
65+
path string
66+
expected bool
67+
}{
68+
{"/home/user/.local/share/pnpm/kernel", true},
69+
{"/home/user/.pnpm/global/kernel", true},
70+
{"/home/user/.npm-global/bin/kernel", false},
71+
}
72+
for _, tt := range tests {
73+
t.Run(tt.path, func(t *testing.T) {
74+
assert.Equal(t, tt.expected, pathMatchesPNPM(tt.path))
75+
})
76+
}
77+
}
78+
79+
func TestPathMatchesHomebrew(t *testing.T) {
80+
tests := []struct {
81+
path string
82+
expected bool
83+
}{
84+
{"/opt/homebrew/bin/kernel", true},
85+
{"/usr/local/Cellar/kernel/1.0/bin/kernel", true},
86+
{"/home/linuxbrew/.linuxbrew/Cellar/kernel/1.0/bin/kernel", true},
87+
{"/home/user/.npm-global/bin/kernel", false},
88+
}
89+
for _, tt := range tests {
90+
t.Run(tt.path, func(t *testing.T) {
91+
assert.Equal(t, tt.expected, pathMatchesHomebrew(tt.path))
92+
})
93+
}
94+
}
95+
96+
func TestInstallMethodRulesPathPrecedence(t *testing.T) {
97+
rules := installMethodRules()
98+
99+
detect := func(path string) InstallMethod {
100+
for _, r := range rules {
101+
if r.check(path) {
102+
return r.method
103+
}
104+
}
105+
return InstallMethodUnknown
106+
}
107+
108+
assert.Equal(t, InstallMethodNPM, detect("/home/user/.npm-global/bin/kernel"))
109+
assert.Equal(t, InstallMethodBun, detect("/home/user/.bun/bin/kernel"))
110+
assert.Equal(t, InstallMethodBrew, detect("/opt/homebrew/bin/kernel"))
111+
assert.Equal(t, InstallMethodPNPM, detect("/home/user/.local/share/pnpm/kernel"))
112+
assert.Equal(t, InstallMethodUnknown, detect("/usr/local/bin/kernel"))
113+
}

0 commit comments

Comments
 (0)