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',
install: { cmd: 'yarn', args: ['install'], cwd: repoDir },
test: { cmd: 'yarn', args: ['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
}));
}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({ 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: 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: { 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',
install: { cmd: 'yarn', args: ['install'], cwd: repoDir },
test: { cmd: 'yarn', args: ['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?