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
19 changes: 19 additions & 0 deletions packages/cli-kit/src/public/node/is-global.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,25 @@ describe('inferPackageManagerForGlobalCLI', () => {
expect(got).toBe('homebrew')
})

test('returns bun when symlink under ~/.bun/bin resolves out of the bun install dir', async () => {
// Given: `bun add -g @shopify/cli` creates ~/.bun/bin/shopify as a symlink to
// ../../node_modules/@shopify/cli/bin/run.js, which on most setups resolves to
// <home>/node_modules/@shopify/cli/bin/run.js — a path that does NOT contain "bun".
// Without inspecting the original symlink path we'd fall through to npm and the
// autoupgrade flow would shell out to `npm install -g` instead of `bun add -g`.
const symlinkPath = '/users/fonso/.bun/bin/shopify'
const realBunPath = '/users/fonso/node_modules/@shopify/cli/bin/run.js'
const argv = ['node', symlinkPath, 'shopify']

vi.mocked(realpathSync).mockImplementationOnce(() => realBunPath)

// When
const got = inferPackageManagerForGlobalCLI(argv)

// Then
expect(got).toBe('bun')
})

test('defaults to npm if realpath fails and no other indicator is present', async () => {
// Given: A path that realpathSync cannot resolve
const nonExistentPath = '/opt/homebrew/bin/shopify'
Expand Down
16 changes: 12 additions & 4 deletions packages/cli-kit/src/public/node/is-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,27 @@ export function inferPackageManagerForGlobalCLI(argv = process.argv, env = proce
}

const processArgv = argv[1] ?? ''
const symlinkPath = processArgv.toLowerCase()

// Resolve symlinks to get the real path of the binary.
let realPath = processArgv.toLowerCase()
let realPath = symlinkPath
try {
realPath = realpathSync(processArgv).toLowerCase()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// fall back to using the original path for detection
}

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

if (matches('yarn')) return 'yarn'
if (matches('pnpm')) return 'pnpm'
if (matches('bun')) return 'bun'

// Check for Homebrew via Cellar path (resolved symlink)
if (realPath.includes('/cellar/')) return 'homebrew'
Expand Down
Loading