Skip to content

Commit e23c9d0

Browse files
Merge pull request #7556 from Shopify/bun-autoupgrade-uses-bun-not-npm
Use bun for autoupgrade when CLI installed via bun add -g
2 parents dd4e5c1 + bde4503 commit e23c9d0

2 files changed

Lines changed: 31 additions & 4 deletions

File tree

packages/cli-kit/src/public/node/is-global.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,25 @@ describe('inferPackageManagerForGlobalCLI', () => {
246246
expect(got).toBe('homebrew')
247247
})
248248

249+
test('returns bun when symlink under ~/.bun/bin resolves out of the bun install dir', async () => {
250+
// Given: `bun add -g @shopify/cli` creates ~/.bun/bin/shopify as a symlink to
251+
// ../../node_modules/@shopify/cli/bin/run.js, which on most setups resolves to
252+
// <home>/node_modules/@shopify/cli/bin/run.js — a path that does NOT contain "bun".
253+
// Without inspecting the original symlink path we'd fall through to npm and the
254+
// autoupgrade flow would shell out to `npm install -g` instead of `bun add -g`.
255+
const symlinkPath = '/users/fonso/.bun/bin/shopify'
256+
const realBunPath = '/users/fonso/node_modules/@shopify/cli/bin/run.js'
257+
const argv = ['node', symlinkPath, 'shopify']
258+
259+
vi.mocked(realpathSync).mockImplementationOnce(() => realBunPath)
260+
261+
// When
262+
const got = inferPackageManagerForGlobalCLI(argv)
263+
264+
// Then
265+
expect(got).toBe('bun')
266+
})
267+
249268
test('defaults to npm if realpath fails and no other indicator is present', async () => {
250269
// Given: A path that realpathSync cannot resolve
251270
const nonExistentPath = '/opt/homebrew/bin/shopify'

packages/cli-kit/src/public/node/is-global.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,27 @@ export function inferPackageManagerForGlobalCLI(argv = process.argv, env = proce
105105
}
106106

107107
const processArgv = argv[1] ?? ''
108+
const symlinkPath = processArgv.toLowerCase()
108109

109110
// Resolve symlinks to get the real path of the binary.
110-
let realPath = processArgv.toLowerCase()
111+
let realPath = symlinkPath
111112
try {
112113
realPath = realpathSync(processArgv).toLowerCase()
113114
// eslint-disable-next-line no-catch-all/no-catch-all
114115
} catch (error) {
115116
// fall back to using the original path for detection
116117
}
117118

118-
if (realPath.includes('yarn')) return 'yarn'
119-
if (realPath.includes('pnpm')) return 'pnpm'
120-
if (realPath.includes('bun')) return 'bun'
119+
// Inspect both the (unresolved) symlink path and the resolved real path. Some
120+
// package managers — notably bun (`~/.bun/bin/<name>`) — install global binaries
121+
// as symlinks pointing into a generic `node_modules` directory whose real path
122+
// no longer contains the package manager name. The original symlink under the
123+
// PM's bin dir is the most reliable signal in that case.
124+
const matches = (needle: string) => realPath.includes(needle) || symlinkPath.includes(needle)
125+
126+
if (matches('yarn')) return 'yarn'
127+
if (matches('pnpm')) return 'pnpm'
128+
if (matches('bun')) return 'bun'
121129

122130
// Check for Homebrew via Cellar path (resolved symlink)
123131
if (realPath.includes('/cellar/')) return 'homebrew'

0 commit comments

Comments
 (0)