Wrap the Full Lifecycle in a Function
Every working script eventually wants to be a function. Ours is no exception.
Sandboxes stop automatically when their configured timeout expires; the default session timeout is five minutes. Prompt cleanup still matters because idle time consumes resources until that safeguard runs. Before we hand this off to a CLI, we'll wrap the lifecycle in try/finally so stop() runs as soon as the work finishes or throws.
Outcome
Refactor the script into an exported runSandboxLifecycle(repoUrl) function that wraps create → clone → read in try/finally, always calls stop(), and returns a structured result.
Fast Track
- Extract the body into
export async function runSandboxLifecycle(repoUrl: string). - Wrap the work in
try/finallywithsandbox.stop()infinally. - Return
{ sandboxName, cloneExitCode, files, readmePreview }.
Hands-on exercise
Restructure src/sandbox-lifecycle.ts. We're keeping all the logic from the last lesson, just reorganizing it.
import { Sandbox } from '@vercel/sandbox';
export type LifecycleResult = {
sandboxName: string;
cloneExitCode: number;
files: string;
readmePreview: string;
};
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 ls = await sandbox.runCommand('ls', ['-la', 'repo']);
let readmePreview = '(no README found)';
const readme = await sandbox.readFileToBuffer({ path: 'repo/README.md' });
if (readme) {
readmePreview = readme.toString('utf8').slice(0, 300);
}
return {
sandboxName: sandbox.name,
cloneExitCode: clone.exitCode,
files: await ls.stdout(),
readmePreview
};
} finally {
await sandbox.stop();
}
}Two changes worth pointing out. First, a failed clone now throws instead of returning early. That lets the caller decide how to handle it, and finally still cleans up the Sandbox either way. Second, we removed the console.log calls. Logging is the CLI's job, not the lifecycle's.
To verify the function still works end-to-end, add a small test caller below the function (we'll delete this when the CLI takes over):
async function main() {
const result = await runSandboxLifecycle('https://github.com/vercel/examples');
console.log(result);
}
main();Try It
pnpm tsx src/sandbox-lifecycle.tsExpected output:
{
sandboxName: 'repo-review-...',
cloneExitCode: 0,
files: 'total 32\ndrwxr-xr-x ... README.md\n...',
readmePreview: '# Vercel Examples\n\nThis repository contains...'
}One object, ready to be consumed by something else. That something else is the CLI we build next.
Commit
git add src/sandbox-lifecycle.ts
git commit -m "feat(sandbox): wrap lifecycle in a reusable function with try/finally"Done-When
runSandboxLifecycle(repoUrl)is exported and accepts a URL- Body is wrapped in
try/finally sandbox.stop()runs infinallyeven on throw- Returns
{ sandboxName, cloneExitCode, files, readmePreview }
Solution
import { Sandbox } from '@vercel/sandbox';
export type LifecycleResult = {
sandboxName: string;
cloneExitCode: number;
files: string;
readmePreview: string;
};
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 ls = await sandbox.runCommand('ls', ['-la', 'repo']);
let readmePreview = '(no README found)';
const readme = await sandbox.readFileToBuffer({ path: 'repo/README.md' });
if (readme) {
readmePreview = readme.toString('utf8').slice(0, 300);
}
return {
sandboxName: sandbox.name,
cloneExitCode: clone.exitCode,
files: await ls.stdout(),
readmePreview
};
} finally {
await sandbox.stop();
}
}Was this helpful?