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
- 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: 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:
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.
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.
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:
pnpm review https://github.com/<some-npm-repo>Expected output:
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
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
yarnfor 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: 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`
};
}Was this helpful?