Skip to content

Commit 2c0acf5

Browse files
EurFeluxclaude
andcommitted
fix: allow global package manager commands in projects with different packageManager
Global operations (e.g., `npm install -g`, `pnpm add -g`, `yarn global add`) now bypass the project-level `packageManager` field check, since they operate outside of the project scope. Fixes #690 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent ec42596 commit 2c0acf5

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed

sources/Engine.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,31 @@ export async function activatePackageManager(lastKnownGood: Record<string, strin
9393
await createLastKnownGoodFile(lastKnownGood);
9494
}
9595

96+
/**
97+
* Checks if the command is a global operation.
98+
* Global operations should be transparent since they operate outside
99+
* of the project scope.
100+
*/
101+
function isGlobalCommand(packageManager: SupportedPackageManagers, args: Array<string>): boolean {
102+
switch (packageManager) {
103+
// npm/pnpm: any command with -g or --global flag
104+
case SupportedPackageManagers.Npm:
105+
case SupportedPackageManagers.Pnpm:
106+
return args.includes(`-g`) || args.includes(`--global`);
107+
108+
// yarn: yarn global <command>
109+
// Note: `yarn global` is only available in Yarn 1.x. If a newer version is used,
110+
// Yarn itself will report an appropriate error.
111+
case SupportedPackageManagers.Yarn:
112+
return args[0] === `global`;
113+
114+
default:
115+
// If a new package manager is added, TypeScript will error here
116+
// reminding us to handle it
117+
throw new Error(`Unhandled package manager: ${packageManager satisfies never}`);
118+
}
119+
}
120+
96121
export class Engine {
97122
constructor(public config: Config = defaultConfig as Config) {
98123
}
@@ -334,6 +359,12 @@ export class Engine {
334359
}
335360
}
336361

362+
// Global operations (install -g, uninstall -g) should be transparent
363+
// since they operate outside of the project scope
364+
if (!isTransparentCommand) {
365+
isTransparentCommand = isGlobalCommand(packageManager, args);
366+
}
367+
337368
const fallbackReference = isTransparentCommand
338369
? definition.transparent.default ?? defaultVersion
339370
: defaultVersion;

tests/main.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,95 @@ it(`should allow using transparent commands on npm-configured projects`, async (
621621
});
622622
});
623623

624+
describe(`should allow global install/uninstall commands in projects configured for a different package manager`, () => {
625+
it(`npm install -g in yarn project`, async () => {
626+
await xfs.mktempPromise(async cwd => {
627+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
628+
packageManager: `[email protected]`,
629+
});
630+
631+
// npm install -g --help should work (we use --help to avoid actual installation)
632+
await expect(runCli(cwd, [`npm`, `install`, `-g`, `--help`])).resolves.toMatchObject({
633+
exitCode: 0,
634+
});
635+
});
636+
});
637+
638+
it(`npm uninstall --global in yarn project`, async () => {
639+
await xfs.mktempPromise(async cwd => {
640+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
641+
packageManager: `[email protected]`,
642+
});
643+
644+
await expect(runCli(cwd, [`npm`, `uninstall`, `--global`, `--help`])).resolves.toMatchObject({
645+
exitCode: 0,
646+
});
647+
});
648+
});
649+
650+
it(`npm i -g in pnpm project`, async () => {
651+
await xfs.mktempPromise(async cwd => {
652+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
653+
packageManager: `[email protected]`,
654+
});
655+
656+
await expect(runCli(cwd, [`npm`, `i`, `-g`, `--help`])).resolves.toMatchObject({
657+
exitCode: 0,
658+
});
659+
});
660+
});
661+
662+
it(`pnpm add -g in yarn project`, async () => {
663+
await xfs.mktempPromise(async cwd => {
664+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
665+
packageManager: `[email protected]`,
666+
});
667+
668+
await expect(runCli(cwd, [`pnpm`, `add`, `-g`, `--help`])).resolves.toMatchObject({
669+
exitCode: 0,
670+
});
671+
});
672+
});
673+
674+
it(`pnpm remove --global in npm project`, async () => {
675+
await xfs.mktempPromise(async cwd => {
676+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
677+
packageManager: `[email protected]`,
678+
});
679+
680+
await expect(runCli(cwd, [`pnpm`, `remove`, `--global`, `--help`])).resolves.toMatchObject({
681+
exitCode: 0,
682+
});
683+
});
684+
});
685+
686+
it(`yarn global add in npm project`, async () => {
687+
await xfs.mktempPromise(async cwd => {
688+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
689+
packageManager: `[email protected]`,
690+
});
691+
692+
// yarn global add should not be blocked by project packageManager
693+
// Note: If a Yarn version that doesn't support `global` is used,
694+
// Yarn itself will report an appropriate error.
695+
const result = await runCli(cwd, [`yarn`, `global`, `add`, `does-not-exist-pkg-12345`]);
696+
expect(result.stderr).not.toContain(`This project is configured to use`);
697+
});
698+
});
699+
700+
it(`yarn global remove in pnpm project`, async () => {
701+
await xfs.mktempPromise(async cwd => {
702+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
703+
packageManager: `[email protected]`,
704+
});
705+
706+
// yarn global remove should not be blocked by project packageManager
707+
const result = await runCli(cwd, [`yarn`, `global`, `remove`, `does-not-exist-pkg-12345`]);
708+
expect(result.stderr).not.toContain(`This project is configured to use`);
709+
});
710+
});
711+
});
712+
624713
it(`should transparently use the preconfigured version when there is no local project`, async () => {
625714
await xfs.mktempPromise(async cwd => {
626715
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({

0 commit comments

Comments
 (0)