---
title: "Connect Analysis to the CLI"
description: "Extend the lifecycle to collect a curated set of files from the cloned repo, pass them to `analyzeRepository`, and print the resulting findings. This is where the two halves of the course finally talk to each other."
canonical_url: "https://vercel.com/academy/vercel-sandbox/connect-analysis-to-the-cli"
md_url: "https://vercel.com/academy/vercel-sandbox/connect-analysis-to-the-cli.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-05-17T17:56:03.892Z"
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>

# Connect Analysis to the CLI

# 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

1. In `src/sandbox-lifecycle.ts`, collect `package.json` and a few source files into `files: Array<{ path, content }>`.
2. Return `files` from the lifecycle alongside the existing result fields.
3. In `src/cli.ts`, call `analyzeRepository(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`:

```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:

```ts
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:

```ts title="src/analyze.ts (delete this block)"
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.

\*\*Warning: Troubleshooting: zero files collected\*\*

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.

\*\*Note: Troubleshooting: long analysis times\*\*

`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

```bash
pnpm review https://github.com/vercel/examples
```

Expected output:

```txt
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

```bash
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 of `src/analyze.ts` is deleted

## Solution

```ts title="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();
  }
}
```


---

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