Skip to content

Commit 78f32a4

Browse files
Fix Windows auto-upgrade for local CLI installs
`currentProcessIsGlobal()` decides between global and local installs by comparing the project root against `process.argv[1]`. The project root flows through `getProjectDir` -> `findPathUpSync` -> `normalizePath` (pathe), which always returns forward slashes on every platform. `process.argv[1]` is OS-native, so on Windows it arrives backslash-separated and the raw `startsWith` comparison ends up as: binDir: C:\\Users\\me\\proj\\node_modules\\@Shopify\\cli\\bin\\run.js projectDir: C:/Users/me/proj binDir.startsWith(projectDir) => false The local install was misclassified as global, so the postrun hook ran `npm install -g @shopify/cli@latest` after every command on Windows even when @shopify/cli was declared as a project dependency. Switches the comparison to `isSubpath` (pathe.relative under the hood) which tolerates either separator, and bails early if argv[1] is empty so we don't accidentally classify it as 'in any directory'. Adds a regression test that constructs the exact Windows shape (forward- slash projectDir vs. backslash argv[1]) — the existing POSIX-shaped tests happened to agree on slashes when CI runs on Linux, so they didn't catch this. Reported by Nick Wesselman; root cause analysis by River.
1 parent 95e9788 commit 78f32a4

3 files changed

Lines changed: 33 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/cli-kit': patch
3+
---
4+
5+
Fix Windows-only bug where `currentProcessIsGlobal()` misclassified a project-local Shopify CLI install as global, causing the auto-upgrade flow to run `npm install -g @shopify/cli@latest` after every command on Windows. The path comparison now goes through `isSubpath` (pathe-based) so backslash-separated `argv[1]` and forward-slash `projectDir` compare correctly. macOS and Linux were unaffected.

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@ describe('currentProcessIsGlobal', () => {
8181
// Then
8282
expect(got).toBeFalsy()
8383
})
84+
85+
test('returns false on Windows when argv uses backslashes and projectDir uses forward slashes', () => {
86+
// Given - mimic the exact Windows shape:
87+
// projectDir comes from pathe -> normalized forward slashes
88+
// argv[1] is OS-native -> backslash separated
89+
const winProjectDir = 'C:/Users/me/project'
90+
const winArgv1 = 'C:\\Users\\me\\project\\node_modules\\@shopify\\cli\\bin\\run.js'
91+
vi.mocked(findPathUpSync).mockReturnValue(`${winProjectDir}/shopify.app.toml`)
92+
const argv = ['node', winArgv1, 'shopify']
93+
94+
// When
95+
const got = currentProcessIsGlobal(argv)
96+
97+
// Then - regression test for the path-separator bug that misclassified
98+
// Windows local installs as global, triggering an unwanted `npm install -g`.
99+
expect(got).toBeFalsy()
100+
})
84101
})
85102

86103
describe('inferPackageManagerForGlobalCLI', () => {

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {cwd, dirname, joinPath, sniffForPath} from './path.js'
1+
import {cwd, dirname, isSubpath, joinPath, sniffForPath} from './path.js'
22
import {isUnitTest} from './context/local.js'
33
import {findPathUpSync, globSync} from './fs.js'
44
import {realpathSync} from 'fs'
@@ -27,9 +27,17 @@ export function currentProcessIsGlobal(argv = process.argv): boolean {
2727

2828
// From node docs: "The second element [of the array] will be the path to the JavaScript file being executed"
2929
const binDir = argv[1] ?? ''
30+
if (!binDir) {
31+
return true
32+
}
3033

31-
// If binDir starts with projectDir, then we are running a local CLI
32-
const isLocal = binDir.startsWith(projectDir.trim())
34+
// If binDir lives inside projectDir, we are running a local CLI.
35+
// Use isSubpath (pathe.relative under the hood) instead of a raw
36+
// string startsWith: projectDir flows through normalizePath and is
37+
// forward-slash on every platform, while argv[1] is OS-native, so on
38+
// Windows it arrives backslash-separated and a naive startsWith would
39+
// misclassify a local install as global.
40+
const isLocal = isSubpath(projectDir.trim(), binDir)
3341

3442
_isGlobal = !isLocal
3543
return _isGlobal

0 commit comments

Comments
 (0)