From 78f32a49839824cb376fc9afad422c13450ff3f4 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 15 May 2026 10:39:26 +0200 Subject: [PATCH 1/2] Fix Windows auto-upgrade for local CLI installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- .changeset/fix-windows-autoupgrade-local-dep.md | 5 +++++ .../cli-kit/src/public/node/is-global.test.ts | 17 +++++++++++++++++ packages/cli-kit/src/public/node/is-global.ts | 14 +++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-windows-autoupgrade-local-dep.md diff --git a/.changeset/fix-windows-autoupgrade-local-dep.md b/.changeset/fix-windows-autoupgrade-local-dep.md new file mode 100644 index 00000000000..1f385451939 --- /dev/null +++ b/.changeset/fix-windows-autoupgrade-local-dep.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': patch +--- + +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. 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 4e2e0b91e9e..53bd98352fc 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 ac0a1e68958..068e61f95fd 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 From 5ce9ede183afb716fb2f867f84986b815201def7 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 15 May 2026 10:43:51 +0200 Subject: [PATCH 2/2] Drop changeset --- .changeset/fix-windows-autoupgrade-local-dep.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/fix-windows-autoupgrade-local-dep.md diff --git a/.changeset/fix-windows-autoupgrade-local-dep.md b/.changeset/fix-windows-autoupgrade-local-dep.md deleted file mode 100644 index 1f385451939..00000000000 --- a/.changeset/fix-windows-autoupgrade-local-dep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@shopify/cli-kit': patch ---- - -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.