Connect Analysis to the CLI
Two functioning halves of a tool aren't a tool. They're two halves.
In this lesson we bridge them. The lifecycle from Chapter 1 needs to hand the analyzer from Chapter 3 a list of files. The CLI from Chapter 2 needs to print what comes back. None of that is new code; it's wiring.
Outcome
Extend runSandboxLifecycle to collect a small set of "files of interest" from the cloned repo. Update the CLI to call analyzeRepository with those files and print the findings.
Fast Track
- In
src/sandbox-lifecycle.ts, collectpackage.jsonand a few source files intofiles: Array<{ path, content }>. - Return
filesfrom the lifecycle alongside the existing result fields. - In
src/cli.ts, callanalyzeRepository(result.files)after the lifecycle and print the findings.
Hands-on exercise
We're going to limit which files we send to the model. Real repos can have thousands of files; sending all of them blows the context window and costs a fortune. For this course, we'll grab the package manifest plus a few source files.
Update src/sandbox-lifecycle.ts:
import { Sandbox } from '@vercel/sandbox';
const INTERESTING_PATHS = [
'repo/package.json',
'repo/src/index.ts',
'repo/src/app.ts',
'repo/lib/auth.ts'
];
export type LifecycleResult = {
sandboxId: string;
cloneExitCode: number;
files: Array<{ path: string; content: string }>;
};
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
}
}
return {
sandboxId: sandbox.sandboxId,
cloneExitCode: clone.exitCode,
files
};
} finally {
await sandbox.stop();
}
}A few things shifted. We dropped the ls output and the README preview; neither was going into the analyzer. We added a hardcoded list of paths to try, and we silently skip ones that don't exist (most repos won't have all four). The files array we return is exactly the shape analyzeRepository wants.
The hardcoded path list is the simplest thing that works. In a real tool you'd walk the repo, filter by extension, maybe rank by relevance. For this course, four guesses is enough to demonstrate the flow.
Now update src/cli.ts to call the analyzer:
import { Command } from 'commander';
import { runSandboxLifecycle } from './sandbox-lifecycle';
import { analyzeRepository } from './analyze';
function isValidGitHubRepoUrl(input: string): boolean {
return /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/?$/.test(input);
}
const program = new Command();
program
.name('repo-review')
.description('Clone and review a GitHub repository in a Sandbox')
.version('0.1.0');
program
.command('review <repoUrl>')
.description('Run a Sandbox review against a GitHub repository URL')
.action(async (repoUrl: string) => {
if (!isValidGitHubRepoUrl(repoUrl)) {
console.error(`Invalid GitHub repository URL: ${repoUrl}`);
console.error('Expected format: https://github.com/<owner>/<repo>');
process.exitCode = 2;
return;
}
console.log(`Reviewing ${repoUrl}...`);
try {
const lifecycle = await runSandboxLifecycle(repoUrl);
console.log(`Sandbox: ${lifecycle.sandboxId}`);
console.log(`Collected ${lifecycle.files.length} file(s) for analysis.`);
if (lifecycle.files.length === 0) {
console.log('No files matched the interest list; skipping analysis.');
return;
}
const review = await analyzeRepository(lifecycle.files);
console.log(`Overall risk: ${review.overallRisk}`);
console.log(`Findings: ${review.findings.length}`);
for (const finding of review.findings) {
console.log(` [${finding.severity}] ${finding.summary} (${finding.file})`);
}
} catch (error) {
console.error('Review failed:', error instanceof Error ? error.message : error);
process.exitCode = 1;
}
});
program.parse();The analyzer call sits inside the same try/catch as the lifecycle, so an analysis failure also gets exit code 1. Good.
One more piece of cleanup. Now that the CLI is the real caller, the temporary main() block at the bottom of src/analyze.ts (the one we used to test analyzeRepository in 3.3) needs to go. Otherwise it runs every time the CLI imports the file. Delete it:
async function main() {
const files = [
{
path: 'src/auth.ts',
content: `...`
}
];
const review = await analyzeRepository(files);
console.log(JSON.stringify(review, null, 2));
}
main();After this, src/analyze.ts ends right after analyzeRepository. Same housekeeping we did to sandbox-lifecycle.ts back in 2.3, same reason.
If Collected 0 file(s) shows up against a real repo, your interest list doesn't match its layout. Open the README in the cloned repo by hand and adjust the paths. For this course, picking a repo with a standard Next.js or Node layout works best.
generateObject calls take 5–30 seconds depending on file size. That's normal. If it consistently times out, you may be sending too much context; trim the interest list.
Try It
pnpm review https://github.com/vercel/examplesExpected output:
Reviewing https://github.com/vercel/examples...
Sandbox: sbx_7N2k4A...
Collected 2 file(s) for analysis.
Overall risk: low
Findings: 3
[medium] Missing input validation on shared utility (package.json)
[low] Inconsistent use of optional chaining (src/index.ts)
[low] No timeout configured for outgoing fetches (src/index.ts)The specific findings will vary by run and by repo. What matters is the shape: a number, a risk level, a list of severity-labeled findings tied to actual files.
Commit
git add src/sandbox-lifecycle.ts src/cli.ts
git commit -m "feat(cli): wire analyzer into the review pipeline"Done-When
- Lifecycle returns
files: Array<{ path, content }>instead of a README preview - CLI imports and calls
analyzeRepository(lifecycle.files) - Findings print one per line with severity, summary, and file
- Exit codes (0/1/2) still behave as in 2.4
- Temporary
main()block at the bottom ofsrc/analyze.tsis deleted
Solution
import { Sandbox } from '@vercel/sandbox';
const INTERESTING_PATHS = [
'repo/package.json',
'repo/src/index.ts',
'repo/src/app.ts',
'repo/lib/auth.ts'
];
export type LifecycleResult = {
sandboxId: string;
cloneExitCode: number;
files: Array<{ path: string; content: string }>;
};
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
}
}
return {
sandboxId: sandbox.sandboxId,
cloneExitCode: clone.exitCode,
files
};
} finally {
await sandbox.stop();
}
}Was this helpful?