Detect the Package Manager
Hardcoding pnpm test worked for exactly one kind of repo.
Running pnpm install in a repo with package-lock.json can create a new pnpm lockfile and resolve dependencies differently from the project's intended tool. We need to inspect the cloned repo and pick the matching package manager.
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
- In
src/test-runner.ts, adddetectPackageManager(sandbox)that checks forpnpm-lock.yaml,yarn.lock, orpackage-lock.json. - Return a
{ install, test }command pair for each. - 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:
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: { cmd: string; args: string[]; cwd: string };
test: { cmd: string; args: string[]; cwd: 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: { cmd: 'pnpm', args: ['install'], cwd: repoDir },
test: { cmd: 'pnpm', args: ['test'], cwd: repoDir }
}
},
{
file: 'yarn.lock',
commands: {
name: 'yarn',
// The default Sandbox image has no yarn binary, so Corepack runs it
install: { cmd: 'corepack', args: ['yarn', 'install'], cwd: repoDir },
test: { cmd: 'corepack', args: ['yarn', 'test'], cwd: repoDir }
}
},
{
file: 'package-lock.json',
commands: {
name: 'npm',
install: { cmd: 'npm', args: ['install'], cwd: repoDir },
test: { cmd: 'npm', args: ['test'], cwd: repoDir }
}
}
];
for (const { file, commands } of checks) {
if (await sandbox.fs.exists(`${repoDir}/${file}`)) {
return commands;
}
}
// No lockfile at all: default to npm install as the last-resort fallback
return {
name: 'npm',
install: { cmd: 'npm', args: ['install'], cwd: repoDir },
test: { cmd: 'npm', args: ['test'], cwd: repoDir }
};
}
export function parseTestFailures(output: string): TestFinding[] {
const lines = output.split('\n');
return lines
.map((line) => line.trim())
.filter((line) => FAILURE_MARKERS.some((marker) => line.startsWith(marker)))
.map((line) => ({
severity: 'high' as const,
category: 'test-failure' as const,
summary: 'Automated test failure',
details: line
}));
}Notice that yarn repos don't call yarn directly. The default Sandbox image ships Node.js with npm, pnpm, and Corepack, but no yarn. Running corepack yarn install lets Corepack fetch the yarn version the repo asks for in package.json#packageManager (or Corepack's default when it doesn't ask), then run it. The first yarn run takes a little longer while Corepack downloads it. That's expected.
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:
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 = {
sandboxName: string;
cloneExitCode: number;
files: Array<{ path: string; content: string }>;
testResult: TestResult;
};
export async function runSandboxLifecycle(repoUrl: string): Promise<LifecycleResult> {
const sandbox = await Sandbox.create({
name: `repo-review-${Date.now()}`,
persistent: false,
timeout: 10 * 60 * 1000
});
try {
const clone = await sandbox.runCommand('git', ['clone', '--depth', '1', repoUrl, 'repo']);
if (clone.exitCode !== 0) {
throw new Error(`Clone failed: ${await clone.stderr()}`);
}
const files: Array<{ path: string; content: string }> = [];
for (const fullPath of INTERESTING_PATHS) {
const content = await sandbox.readFileToBuffer({ path: fullPath });
if (content) {
files.push({
path: fullPath.replace(/^repo\//, ''),
content: content.toString('utf8')
});
}
}
const pm = await detectPackageManager(sandbox);
const install = await sandbox.runCommand(pm.install);
if (install.exitCode !== 0) {
throw new Error(`Install failed: ${await install.stderr()}`);
}
const test = await sandbox.runCommand(pm.test);
return {
sandboxName: sandbox.name,
cloneExitCode: clone.exitCode,
files,
testResult: {
exitCode: test.exitCode,
stdout: await test.stdout(),
stderr: await 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.
Try It
Run against a repo with package-lock.json:
pnpm review https://github.com/<some-npm-repo>Expected output:
Reviewing https://github.com/<...>...
Sandbox: repo-review-1792017569082
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
git add src/test-runner.ts src/sandbox-lifecycle.ts
git commit -m "feat(testing): detect package manager from lockfile"Done-When
detectPackageManagerreturnspnpmfor repos withpnpm-lock.yaml- Returns
yarn(run throughcorepack) for repos withyarn.lock - Returns
npmfor repos withpackage-lock.json - Falls back to
npm install(loose) when no lockfile is present - Lifecycle uses the returned commands instead of hardcoded
pnpm
Solution
import type { Sandbox } from '@vercel/sandbox';
export type PackageManagerCommands = {
name: 'pnpm' | 'npm' | 'yarn';
install: { cmd: string; args: string[]; cwd: string };
test: { cmd: string; args: string[]; cwd: 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: { cmd: 'pnpm', args: ['install'], cwd: repoDir },
test: { cmd: 'pnpm', args: ['test'], cwd: repoDir }
}
},
{
file: 'yarn.lock',
commands: {
name: 'yarn',
// The default Sandbox image has no yarn binary, so Corepack runs it
install: { cmd: 'corepack', args: ['yarn', 'install'], cwd: repoDir },
test: { cmd: 'corepack', args: ['yarn', 'test'], cwd: repoDir }
}
},
{
file: 'package-lock.json',
commands: {
name: 'npm',
install: { cmd: 'npm', args: ['install'], cwd: repoDir },
test: { cmd: 'npm', args: ['test'], cwd: repoDir }
}
}
];
for (const { file, commands } of checks) {
if (await sandbox.fs.exists(`${repoDir}/${file}`)) {
return commands;
}
}
return {
name: 'npm',
install: { cmd: 'npm', args: ['install'], cwd: repoDir },
test: { cmd: 'npm', args: ['test'], cwd: repoDir }
};
}Was this helpful?