@@ -4,16 +4,29 @@ import {renderSelectPrompt} from './ui.js'
44import { globalCLIVersion } from './version.js'
55import * as execa from 'execa'
66import { beforeEach , describe , expect , test , vi } from 'vitest'
7+ import { realpathSync } from 'fs'
78
89vi . mock ( './system.js' )
910vi . mock ( './ui.js' )
1011vi . mock ( 'execa' )
1112vi . mock ( 'which' )
1213vi . mock ( './version.js' )
1314
15+ // Mock fs.realpathSync at the module level
16+ vi . mock ( 'fs' , async ( importOriginal ) => {
17+ const actual = await importOriginal < typeof import ( 'fs' ) > ( )
18+ return {
19+ ...actual ,
20+ realpathSync : vi . fn ( ( path : string ) => path ) ,
21+ }
22+ } )
23+
1424const globalNPMPath = '/path/to/global/npm'
1525const globalYarnPath = '/path/to/global/yarn'
1626const globalPNPMPath = '/path/to/global/pnpm'
27+ const globalHomebrewIntel = '/usr/local/Cellar/shopify-cli/3.89.0/bin/shopify'
28+ const globalHomebrewAppleSilicon = '/opt/homebrew/Cellar/shopify-cli/3.89.0/bin/shopify'
29+ const globalHomebrewLinux = '/home/linuxbrew/.linuxbrew/Cellar/shopify-cli/3.89.0/bin/shopify'
1730const unknownGlobalPath = '/path/to/global/unknown'
1831const localProjectPath = '/path/local'
1932
@@ -46,6 +59,12 @@ describe('currentProcessIsGlobal', () => {
4659} )
4760
4861describe ( 'inferPackageManagerForGlobalCLI' , ( ) => {
62+ beforeEach ( ( ) => {
63+ // Reset mock to return the input path by default (no symlink resolution)
64+ vi . mocked ( realpathSync ) . mockClear ( )
65+ vi . mocked ( realpathSync ) . mockImplementation ( ( path ) => String ( path ) )
66+ } )
67+
4968 test ( 'returns yarn if yarn is in path' , async ( ) => {
5069 // Given
5170 const argv = [ 'node' , globalYarnPath , 'shopify' ]
@@ -89,6 +108,115 @@ describe('inferPackageManagerForGlobalCLI', () => {
89108 // Then
90109 expect ( got ) . toBe ( 'unknown' )
91110 } )
111+
112+ test ( 'returns homebrew if SHOPIFY_HOMEBREW_FORMULA is set' , async ( ) => {
113+ // Given
114+ const argv = [ 'node' , globalHomebrewAppleSilicon , 'shopify' ]
115+ const env = { SHOPIFY_HOMEBREW_FORMULA : 'shopify-cli' }
116+
117+ // When
118+ const got = inferPackageManagerForGlobalCLI ( argv , env )
119+
120+ // Then
121+ expect ( got ) . toBe ( 'homebrew' )
122+ } )
123+
124+ test ( 'returns homebrew for Intel Mac Cellar path' , async ( ) => {
125+ // Given
126+ const argv = [ 'node' , globalHomebrewIntel , 'shopify' ]
127+
128+ // When
129+ const got = inferPackageManagerForGlobalCLI ( argv )
130+
131+ // Then
132+ expect ( got ) . toBe ( 'homebrew' )
133+ } )
134+
135+ test ( 'returns homebrew for Apple Silicon Cellar path' , async ( ) => {
136+ // Given
137+ const argv = [ 'node' , globalHomebrewAppleSilicon , 'shopify' ]
138+
139+ // When
140+ const got = inferPackageManagerForGlobalCLI ( argv )
141+
142+ // Then
143+ expect ( got ) . toBe ( 'homebrew' )
144+ } )
145+
146+ test ( 'returns homebrew for Linux Homebrew path' , async ( ) => {
147+ // Given
148+ const argv = [ 'node' , globalHomebrewLinux , 'shopify' ]
149+
150+ // When
151+ const got = inferPackageManagerForGlobalCLI ( argv )
152+
153+ // Then
154+ expect ( got ) . toBe ( 'homebrew' )
155+ } )
156+
157+ test ( 'returns homebrew when HOMEBREW_PREFIX matches path' , async ( ) => {
158+ // Given
159+ const argv = [ 'node' , '/opt/homebrew/bin/shopify' , 'shopify' ]
160+ const env = { HOMEBREW_PREFIX : '/opt/homebrew' }
161+
162+ // When
163+ const got = inferPackageManagerForGlobalCLI ( argv , env )
164+
165+ // Then
166+ expect ( got ) . toBe ( 'homebrew' )
167+ } )
168+
169+ test ( 'resolves symlinks to detect actual package manager (yarn)' , async ( ) => {
170+ // Given: A symlink in /opt/homebrew/bin pointing to yarn global
171+ const symlinkPath = '/opt/homebrew/bin/shopify'
172+ const realYarnPath = '/Users/user/.config/yarn/global/node_modules/.bin/shopify'
173+ const argv = [ 'node' , symlinkPath , 'shopify' ]
174+ const env = { HOMEBREW_PREFIX : '/opt/homebrew' }
175+
176+ // Mock realpathSync to resolve the symlink
177+ vi . mocked ( realpathSync ) . mockReturnValueOnce ( realYarnPath )
178+
179+ // When
180+ const got = inferPackageManagerForGlobalCLI ( argv , env )
181+
182+ // Then: Should detect yarn (from real path), not homebrew (from symlink)
183+ expect ( got ) . toBe ( 'yarn' )
184+ expect ( vi . mocked ( realpathSync ) ) . toHaveBeenCalledWith ( symlinkPath )
185+ } )
186+
187+ test ( 'resolves symlinks to detect real homebrew installation' , async ( ) => {
188+ // Given: A symlink in /opt/homebrew/bin pointing to a Cellar path (real Homebrew)
189+ const symlinkPath = '/opt/homebrew/bin/shopify'
190+ const realHomebrewPath = '/opt/homebrew/Cellar/shopify-cli/3.89.0/bin/shopify'
191+ const argv = [ 'node' , symlinkPath , 'shopify' ]
192+
193+ // Mock realpathSync to resolve the symlink
194+ vi . mocked ( realpathSync ) . mockReturnValueOnce ( realHomebrewPath )
195+
196+ // When
197+ const got = inferPackageManagerForGlobalCLI ( argv )
198+
199+ // Then: Should still detect homebrew from the real Cellar path
200+ expect ( got ) . toBe ( 'homebrew' )
201+ } )
202+
203+ test ( 'falls back to original path if realpath fails' , async ( ) => {
204+ // Given: A path that realpathSync cannot resolve
205+ const nonExistentPath = '/opt/homebrew/bin/shopify'
206+ const argv = [ 'node' , nonExistentPath , 'shopify' ]
207+ const env = { HOMEBREW_PREFIX : '/opt/homebrew' }
208+
209+ // Mock realpathSync to throw an error
210+ vi . mocked ( realpathSync ) . mockImplementationOnce ( ( ) => {
211+ throw new Error ( 'ENOENT: no such file or directory' )
212+ } )
213+
214+ // When
215+ const got = inferPackageManagerForGlobalCLI ( argv , env )
216+
217+ // Then: Should fall back to checking the original path
218+ expect ( got ) . toBe ( 'homebrew' )
219+ } )
92220} )
93221
94222describe ( 'installGlobalCLIPrompt' , ( ) => {
0 commit comments