diff --git a/packages/cli-kit/src/public/node/is-global.test.ts b/packages/cli-kit/src/public/node/is-global.test.ts index 4e2e0b91e9..53bd98352f 100644 --- a/packages/cli-kit/src/public/node/is-global.test.ts +++ b/packages/cli-kit/src/public/node/is-global.test.ts @@ -81,6 +81,23 @@ describe('currentProcessIsGlobal', () => { // Then expect(got).toBeFalsy() }) + + test('returns false on Windows when argv uses backslashes and projectDir uses forward slashes', () => { + // Given - mimic the exact Windows shape: + // projectDir comes from pathe -> normalized forward slashes + // argv[1] is OS-native -> backslash separated + const winProjectDir = 'C:/Users/me/project' + const winArgv1 = 'C:\\Users\\me\\project\\node_modules\\@shopify\\cli\\bin\\run.js' + vi.mocked(findPathUpSync).mockReturnValue(`${winProjectDir}/shopify.app.toml`) + const argv = ['node', winArgv1, 'shopify'] + + // When + const got = currentProcessIsGlobal(argv) + + // Then - regression test for the path-separator bug that misclassified + // Windows local installs as global, triggering an unwanted `npm install -g`. + expect(got).toBeFalsy() + }) }) describe('inferPackageManagerForGlobalCLI', () => { diff --git a/packages/cli-kit/src/public/node/is-global.ts b/packages/cli-kit/src/public/node/is-global.ts index ac0a1e6895..068e61f95f 100644 --- a/packages/cli-kit/src/public/node/is-global.ts +++ b/packages/cli-kit/src/public/node/is-global.ts @@ -1,4 +1,4 @@ -import {cwd, dirname, joinPath, sniffForPath} from './path.js' +import {cwd, dirname, isSubpath, joinPath, sniffForPath} from './path.js' import {isUnitTest} from './context/local.js' import {findPathUpSync, globSync} from './fs.js' import {realpathSync} from 'fs' @@ -27,9 +27,17 @@ export function currentProcessIsGlobal(argv = process.argv): boolean { // From node docs: "The second element [of the array] will be the path to the JavaScript file being executed" const binDir = argv[1] ?? '' + if (!binDir) { + return true + } - // If binDir starts with projectDir, then we are running a local CLI - const isLocal = binDir.startsWith(projectDir.trim()) + // If binDir lives inside projectDir, we are running a local CLI. + // Use isSubpath (pathe.relative under the hood) instead of a raw + // string startsWith: projectDir flows through normalizePath and is + // forward-slash on every platform, while argv[1] is OS-native, so on + // Windows it arrives backslash-separated and a naive startsWith would + // misclassify a local install as global. + const isLocal = isSubpath(projectDir.trim(), binDir) _isGlobal = !isLocal return _isGlobal