fix(cli): prefer project lockfile over invocation package manager#1340
Merged
sorccu merged 4 commits intoJun 3, 2026
Merged
Conversation
detectPackageManager consulted ambient signals (npm_config_user_agent, JS runtime) before the project's lockfile, so `npx checkly` in a pnpm repo detected npm and later failed to find an npm lockfile, and tests run via `pnpm vitest` inherited a pnpm user agent that overrode the fixture's actual lockfile. Detection now runs a lockfile-required pass first: a user-agent/runtime signal only counts when that package manager has a backing lockfile in the lineage. When no lockfile exists anywhere (e.g. outside a repo), it falls back to the previous un-gated order, ultimately npm. cnpm shares npm's package-lock.json, so CNpmDetector now claims it and npm is ordered ahead of cnpm in knownPackageManagers: a bare package-lock.json defaults to npm, while a cnpm user agent alongside one resolves to cnpm. This keeps cnpm detectable without reintroducing the cross-package-manager bug. Also drops the skipUserAgent workaround in boilerplate.ts, now redundant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iles Remove the singular, Promise.any-based detectNearestLockfile (which was nondeterministic for multi-detector calls and only used by initWorkspace) and rename detectAllNearestLockfiles to detectNearestLockfiles as the sole lockfile finder. initWorkspace takes the first result of its single-detector lookup, which is equivalent to the old behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detection tie-breaks depended on the order detectors were supplied in (the knownPackageManagers default or a caller's custom array). Most visibly, the executable-on-PATH tier returned the first detector whose binary was found, and since npm is almost always installed it beat cnpm — npm's presence is no signal, yet it always won. Each detector now carries an intrinsic priority(method) (higher wins), and every tier selects the highest-priority match instead of the first by array position, so input order no longer affects the result. npm deprioritizes its executable (priority 0) so an installed cnpm/pnpm/yarn wins the executable tier, while npm keeps its normal priority elsewhere and still wins the package-lock.json lockfile tie over cnpm and remains the ultimate fallback. The config tier is unchanged (config files are mutually exclusive across package managers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iles
Mirror the detectNearestLockfiles design for config files: replace the
singular, Promise.any-based detectNearestConfigFile (nondeterministic when a
directory has more than one config) with a plural detectNearestConfigFiles that
collects all co-located config files, and have the detection config tier pick
the highest priority('configFile') among them. initWorkspace takes the first
result of its single-detector lookup.
No behavioral change in practice — only pnpm and deno declare a config file and
they never co-occur — but it makes the config tier priority-driven and
order-independent, consistent with every other tier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
detectPackageManagerconsulted ambient signals — thenpm_config_user_agentenv var and the JS runtime — before the project's lockfile. These describe how the CLI was launched, not what the project uses, so:npx checklyin a pnpm repo reported an npm user agent → we detected npm → project parsing then failed to find an npm lockfile.pnpm vitestleaks a pnpm user agent into the CLI process, so tests detected pnpm even when the fixture used a different package manager.Fix
Detection now runs a lockfile-required pass first: a user-agent/runtime signal only counts when that package manager has a backing lockfile in the directory lineage. When no lockfile exists anywhere (e.g. running outside a repo), it falls back to the previous un-gated order, ultimately defaulting to npm. Behavior is therefore unchanged when there's no lockfile, and the lockfile wins whenever one is present.
cnpm
cnpm shares npm's
package-lock.json.CNpmDetectornow claims it so cnpm stays detectable, and:package-lock.jsondefaults to npm;Tie-breaking by priority (not detector order)
Detection tie-breaks previously depended on the order detectors were supplied in. Most visibly, the executable-on-PATH tier returned the first detector whose binary was found — and since npm is almost always installed, it beat cnpm even though its presence is no signal.
Each detector now carries an intrinsic
priority(method)(higher wins), and every tier selects the highest-priority match rather than the first by position, so input order no longer affects the result. npm deprioritizes its executable (so an installed cnpm/pnpm/yarn wins the executable tier) while keeping its normal priority elsewhere — it still wins thepackage-lock.jsonlockfile tie over cnpm and remains the ultimate fallback.Tests
Unit suite (
package-manager.spec.ts, 35 cases) covering every detection tier hermetically (temp dirs, bounded lineage, restricted detectors, spied executables/runtime, saved/restored user agent): lockfile-beats-ambient, no-lockfile fallback, lineage walking, co-located lockfiles, the cnpm cases, executable-tier priority (cnpm beats npm; npm alone still wins), config-file priority, and order-independence (same detectors in different orders yield the same result).Other changes
skipUserAgentworkaround inboilerplate.ts.detectNearestLockfiles/detectNearestConfigFilesfinders (plural, collect-all), removing the nondeterministicPromise.any-based singular variants. Both tiers now select bypriority(method), so they're deterministic and order-independent.🤖 Generated with Claude Code