@@ -4,18 +4,32 @@ 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'
18- const localProjectPath = '/path/local'
31+ // Must be within the actual workspace so currentProcessIsGlobal recognizes it as local
32+ const localProjectPath = `${ process . cwd ( ) } /node_modules/.bin/shopify`
1933
2034beforeEach ( ( ) => {
2135 ; ( vi . mocked ( execa . execaSync ) as any ) . mockReturnValue ( { stdout : localProjectPath } )
@@ -46,6 +60,12 @@ describe('currentProcessIsGlobal', () => {
4660} )
4761
4862describe ( 'inferPackageManagerForGlobalCLI' , ( ) => {
63+ beforeEach ( ( ) => {
64+ // Reset mock to return the input path by default (no symlink resolution)
65+ vi . mocked ( realpathSync ) . mockClear ( )
66+ vi . mocked ( realpathSync ) . mockImplementation ( ( path ) => String ( path ) )
67+ } )
68+
4969 test ( 'returns yarn if yarn is in path' , async ( ) => {
5070 // Given
5171 const argv = [ 'node' , globalYarnPath , 'shopify' ]
@@ -89,6 +109,115 @@ describe('inferPackageManagerForGlobalCLI', () => {
89109 // Then
90110 expect ( got ) . toBe ( 'unknown' )
91111 } )
112+
113+ test ( 'returns homebrew if SHOPIFY_HOMEBREW_FORMULA is set' , async ( ) => {
114+ // Given
115+ const argv = [ 'node' , globalHomebrewAppleSilicon , 'shopify' ]
116+ const env = { SHOPIFY_HOMEBREW_FORMULA : 'shopify-cli' }
117+
118+ // When
119+ const got = inferPackageManagerForGlobalCLI ( argv , env )
120+
121+ // Then
122+ expect ( got ) . toBe ( 'homebrew' )
123+ } )
124+
125+ test ( 'returns homebrew for Intel Mac Cellar path' , async ( ) => {
126+ // Given
127+ const argv = [ 'node' , globalHomebrewIntel , 'shopify' ]
128+
129+ // When
130+ const got = inferPackageManagerForGlobalCLI ( argv )
131+
132+ // Then
133+ expect ( got ) . toBe ( 'homebrew' )
134+ } )
135+
136+ test ( 'returns homebrew for Apple Silicon Cellar path' , async ( ) => {
137+ // Given
138+ const argv = [ 'node' , globalHomebrewAppleSilicon , 'shopify' ]
139+
140+ // When
141+ const got = inferPackageManagerForGlobalCLI ( argv )
142+
143+ // Then
144+ expect ( got ) . toBe ( 'homebrew' )
145+ } )
146+
147+ test ( 'returns homebrew for Linux Homebrew path' , async ( ) => {
148+ // Given
149+ const argv = [ 'node' , globalHomebrewLinux , 'shopify' ]
150+
151+ // When
152+ const got = inferPackageManagerForGlobalCLI ( argv )
153+
154+ // Then
155+ expect ( got ) . toBe ( 'homebrew' )
156+ } )
157+
158+ test ( 'returns homebrew when HOMEBREW_PREFIX matches path' , async ( ) => {
159+ // Given
160+ const argv = [ 'node' , '/opt/homebrew/bin/shopify' , 'shopify' ]
161+ const env = { HOMEBREW_PREFIX : '/opt/homebrew' }
162+
163+ // When
164+ const got = inferPackageManagerForGlobalCLI ( argv , env )
165+
166+ // Then
167+ expect ( got ) . toBe ( 'homebrew' )
168+ } )
169+
170+ test ( 'resolves symlinks to detect actual package manager (yarn)' , async ( ) => {
171+ // Given: A symlink in /opt/homebrew/bin pointing to yarn global
172+ const symlinkPath = '/opt/homebrew/bin/shopify'
173+ const realYarnPath = '/Users/user/.config/yarn/global/node_modules/.bin/shopify'
174+ const argv = [ 'node' , symlinkPath , 'shopify' ]
175+ const env = { HOMEBREW_PREFIX : '/opt/homebrew' }
176+
177+ // Mock realpathSync to resolve the symlink
178+ vi . mocked ( realpathSync ) . mockReturnValueOnce ( realYarnPath )
179+
180+ // When
181+ const got = inferPackageManagerForGlobalCLI ( argv , env )
182+
183+ // Then: Should detect yarn (from real path), not homebrew (from symlink)
184+ expect ( got ) . toBe ( 'yarn' )
185+ expect ( vi . mocked ( realpathSync ) ) . toHaveBeenCalledWith ( symlinkPath )
186+ } )
187+
188+ test ( 'resolves symlinks to detect real homebrew installation' , async ( ) => {
189+ // Given: A symlink in /opt/homebrew/bin pointing to a Cellar path (real Homebrew)
190+ const symlinkPath = '/opt/homebrew/bin/shopify'
191+ const realHomebrewPath = '/opt/homebrew/Cellar/shopify-cli/3.89.0/bin/shopify'
192+ const argv = [ 'node' , symlinkPath , 'shopify' ]
193+
194+ // Mock realpathSync to resolve the symlink
195+ vi . mocked ( realpathSync ) . mockReturnValueOnce ( realHomebrewPath )
196+
197+ // When
198+ const got = inferPackageManagerForGlobalCLI ( argv )
199+
200+ // Then: Should still detect homebrew from the real Cellar path
201+ expect ( got ) . toBe ( 'homebrew' )
202+ } )
203+
204+ test ( 'falls back to original path if realpath fails' , async ( ) => {
205+ // Given: A path that realpathSync cannot resolve
206+ const nonExistentPath = '/opt/homebrew/bin/shopify'
207+ const argv = [ 'node' , nonExistentPath , 'shopify' ]
208+ const env = { HOMEBREW_PREFIX : '/opt/homebrew' }
209+
210+ // Mock realpathSync to throw an error
211+ vi . mocked ( realpathSync ) . mockImplementationOnce ( ( ) => {
212+ throw new Error ( 'ENOENT: no such file or directory' )
213+ } )
214+
215+ // When
216+ const got = inferPackageManagerForGlobalCLI ( argv , env )
217+
218+ // Then: Should fall back to checking the original path
219+ expect ( got ) . toBe ( 'homebrew' )
220+ } )
92221} )
93222
94223describe ( 'installGlobalCLIPrompt' , ( ) => {
0 commit comments