---
title: "Handle Package Manager Variants"
description: "Not every repo uses pnpm. In this lesson, we detect the lockfile inside the cloned repo, pick `pnpm`/`npm`/`yarn` accordingly, and stop hardcoding the tool name."
canonical_url: "https://vercel.com/academy/vercel-sandbox/handle-package-manager-variants"
md_url: "https://vercel.com/academy/vercel-sandbox/handle-package-manager-variants.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-05-18T19:30:16.751Z"
content_type: "lesson"
course: "vercel-sandbox"
course_title: "Vercel Sandbox"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Handle Package Manager Variants

# Detect the Package Manager

Hardcoding `pnpm test` worked for exactly one kind of repo.

The first time you run the CLI against a repo with a `package-lock.json`, `pnpm install` errors out about a missing lockfile, and the whole pipeline is wrong before the analyzer even runs. We need to look at what's actually in the cloned repo and pick the matching tool.

## Outcome

Add a `detectPackageManager(sandbox)` helper to `src/test-runner.ts` that reads which lockfile the repo has and returns the matching package manager. Update the lifecycle to use it.

## Fast Track

1. In `src/test-runner.ts`, add `detectPackageManager(sandbox)` that checks for `pnpm-lock.yaml`, `yarn.lock`, or `package-lock.json`.
2. Return a `{ install, test }` command pair for each.
3. In `src/sandbox-lifecycle.ts`, call the helper and use its commands.

## Hands-on exercise

Open `src/test-runner.ts` and add the detection helper:

```ts
import type { Sandbox } from '@vercel/sandbox';

export type TestFinding = {
  severity: 'medium' | 'high';
  category: 'test-failure';
  summary: string;
  details: string;
};

export type PackageManagerCommands = {
  name: 'pnpm' | 'npm' | 'yarn';
  install: string;
  test: string;
};

const FAILURE_MARKERS = ['FAIL ', '✕ ', '× '];

export async function detectPackageManager(
  sandbox: Sandbox,
  repoDir = 'repo'
): Promise<PackageManagerCommands> {
  const checks: Array<{ file: string; commands: PackageManagerCommands }> = [
    {
      file: 'pnpm-lock.yaml',
      commands: {
        name: 'pnpm',
        install: `cd ${repoDir} && pnpm install`,
        test: `cd ${repoDir} && pnpm test`
      }
    },
    {
      file: 'yarn.lock',
      commands: {
        name: 'yarn',
        install: `cd ${repoDir} && yarn install`,
        test: `cd ${repoDir} && yarn test`
      }
    },
    {
      file: 'package-lock.json',
      commands: {
        name: 'npm',
        install: `cd ${repoDir} && npm install`,
        test: `cd ${repoDir} && npm test`
      }
    }
  ];

  for (const { file, commands } of checks) {
    try {
      await sandbox.readFile(`${repoDir}/${file}`);
      return commands;
    } catch {
      // lockfile not present; try the next one
    }
  }

  // No lockfile at all: default to npm install as the last-resort fallback
  return {
    name: 'npm',
    install: `cd ${repoDir} && npm install`,
    test: `cd ${repoDir} && npm test`
  };
}

export function parseTestFailures(output: string): TestFinding[] {
  const lines = output.split('\n');

  return lines
    .map((line) => line.trim())
    .filter((line) => FAILURE_MARKERS.some((marker) => line.includes(marker)))
    .map((line) => ({
      severity: 'high' as const,
      category: 'test-failure' as const,
      summary: 'Automated test failure',
      details: line
    }));
}
```

The detection runs through the lockfile candidates in order and stops at the first match. If none of them match, we fall back to `npm install` without a lockfile, knowing the result may not be reproducible. That fallback exists so the tool doesn't crash on weird repos; it's not a real recommendation.

Now update `src/sandbox-lifecycle.ts` to use it:

```ts
import { Sandbox } from '@vercel/sandbox';
import { detectPackageManager } from './test-runner';

const INTERESTING_PATHS = [
  'repo/package.json',
  'repo/src/index.ts',
  'repo/src/app.ts',
  'repo/lib/auth.ts'
];

export type TestResult = {
  exitCode: number;
  stdout: string;
  stderr: string;
  packageManager: string;
};

export type LifecycleResult = {
  sandboxId: string;
  cloneExitCode: number;
  files: Array<{ path: string; content: string }>;
  testResult: TestResult;
};

export async function runSandboxLifecycle(repoUrl: string): Promise<LifecycleResult> {
  const sandbox = await Sandbox.create();

  try {
    const clone = await sandbox.runCommand(`git clone ${repoUrl} repo`);
    if (clone.exitCode !== 0) {
      throw new Error(`Clone failed: ${clone.stderr}`);
    }

    const files: Array<{ path: string; content: string }> = [];
    for (const fullPath of INTERESTING_PATHS) {
      try {
        const content = await sandbox.readFile(fullPath);
        files.push({
          path: fullPath.replace(/^repo\//, ''),
          content
        });
      } catch {
        // file doesn't exist in this repo; skip it
      }
    }

    const pm = await detectPackageManager(sandbox);
    await sandbox.runCommand(pm.install);
    const test = await sandbox.runCommand(pm.test);

    return {
      sandboxId: sandbox.sandboxId,
      cloneExitCode: clone.exitCode,
      files,
      testResult: {
        exitCode: test.exitCode,
        stdout: test.stdout,
        stderr: test.stderr,
        packageManager: pm.name
      }
    };
  } finally {
    await sandbox.stop();
  }
}
```

The lifecycle no longer cares which package manager the repo uses. It asks, gets back a command pair, and runs them.

\*\*Warning: Troubleshooting: detection returns wrong manager\*\*

Some repos have multiple lockfiles (`pnpm-lock.yaml` and `package-lock.json`). The detection picks the first one in the priority order, which may not match what the repo's contributors actually use. If you hit this, swap the order in the `checks` array.

\*\*Note: Troubleshooting: stricter installs in production\*\*

We're using plain `install` commands for simplicity. In a production tool you'd usually want stricter variants (`pnpm install --frozen-lockfile`, `npm ci`, `yarn install --immutable`) so a manipulated lockfile fails loudly instead of being silently rewritten.

## Try It

Run against a repo with `package-lock.json`:

```bash
pnpm review https://github.com/<some-npm-repo>
```

Expected output:

```txt
Reviewing https://github.com/<...>...
Sandbox: sbx_7N2k4A...
Collected 2 file(s) for analysis.
Overall risk: low
Findings: 2
  ...
```

The tests should still run, even though the repo isn't a pnpm project. If you add a temporary log of `lifecycle.testResult.packageManager`, you'll see `"npm"` instead of `"pnpm"`.

## Commit

```bash
git add src/test-runner.ts src/sandbox-lifecycle.ts
git commit -m "feat(testing): detect package manager from lockfile"
```

## Done-When

- [ ] `detectPackageManager` returns `pnpm` for repos with `pnpm-lock.yaml`
- [ ] Returns `yarn` for repos with `yarn.lock`
- [ ] Returns `npm` for repos with `package-lock.json`
- [ ] Falls back to `npm install` (loose) when no lockfile is present
- [ ] Lifecycle uses the returned commands instead of hardcoded `pnpm`

## Solution

```ts title="src/test-runner.ts (relevant additions)"
import type { Sandbox } from '@vercel/sandbox';

export type PackageManagerCommands = {
  name: 'pnpm' | 'npm' | 'yarn';
  install: string;
  test: string;
};

export async function detectPackageManager(
  sandbox: Sandbox,
  repoDir = 'repo'
): Promise<PackageManagerCommands> {
  const checks: Array<{ file: string; commands: PackageManagerCommands }> = [
    {
      file: 'pnpm-lock.yaml',
      commands: {
        name: 'pnpm',
        install: `cd ${repoDir} && pnpm install`,
        test: `cd ${repoDir} && pnpm test`
      }
    },
    {
      file: 'yarn.lock',
      commands: {
        name: 'yarn',
        install: `cd ${repoDir} && yarn install`,
        test: `cd ${repoDir} && yarn test`
      }
    },
    {
      file: 'package-lock.json',
      commands: {
        name: 'npm',
        install: `cd ${repoDir} && npm install`,
        test: `cd ${repoDir} && npm test`
      }
    }
  ];

  for (const { file, commands } of checks) {
    try {
      await sandbox.readFile(`${repoDir}/${file}`);
      return commands;
    } catch {
      // try the next one
    }
  }

  return {
    name: 'npm',
    install: `cd ${repoDir} && npm install`,
    test: `cd ${repoDir} && npm test`
  };
}
```


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
