Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Runnable examples live in [`examples/`](./examples).
## Table of Contents

- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle)
- [Devbox Snapshot and Resume](#devbox-snapshot-resume)
- [MCP Hub + Claude Code + GitHub](#mcp-github-tools)

<a id="devbox-from-blueprint-lifecycle"></a>
Expand Down Expand Up @@ -39,6 +40,37 @@ yarn test:examples

**Source:** [`examples/devbox-from-blueprint-lifecycle.ts`](./examples/devbox-from-blueprint-lifecycle.ts)

<a id="devbox-snapshot-resume"></a>
## Devbox Snapshot and Resume

**Use case:** Create a devbox, snapshot its disk, resume from the snapshot, and demonstrate that changes in the original devbox do not affect the clone.

**Tags:** `devbox`, `snapshot`, `resume`, `cleanup`

### Workflow
- Create a devbox
- Write a file to the devbox
- Create a disk snapshot
- Create a new devbox from the snapshot
- Modify the file on the original devbox
- Verify the clone has the original content
- Shutdown both devboxes and delete the snapshot

### Prerequisites
- `RUNLOOP_API_KEY`

### Run
```sh
yarn tsn -T examples/devbox-snapshot-resume.ts
```

### Test
```sh
yarn test:examples
```

**Source:** [`examples/devbox-snapshot-resume.ts`](./examples/devbox-snapshot-resume.ts)

<a id="mcp-github-tools"></a>
## MCP Hub + Claude Code + GitHub

Expand Down
127 changes: 127 additions & 0 deletions examples/devbox-snapshot-resume.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env -S npm run tsn -T

/**
---
title: Devbox Snapshot and Resume
slug: devbox-snapshot-resume
use_case: Create a devbox, snapshot its disk, resume from the snapshot, and demonstrate that changes in the original devbox do not affect the clone.
workflow:
- Create a devbox
- Write a file to the devbox
- Create a disk snapshot
- Create a new devbox from the snapshot
- Modify the file on the original devbox
- Verify the clone has the original content
- Shutdown both devboxes and delete the snapshot
tags:
- devbox
- snapshot
- resume
- cleanup
prerequisites:
- RUNLOOP_API_KEY
run: yarn tsn -T examples/devbox-snapshot-resume.ts
test: yarn test:examples
---
*/

import { RunloopSDK } from '@runloop/api-client';
import { wrapRecipe, runAsCli } from './_harness';
import type { RecipeContext, RecipeOutput } from './types';

const FILE_PATH = '/home/user/welcome.txt';
const ORIGINAL_CONTENT = 'hello world!';
const MODIFIED_CONTENT = 'original devbox has changed the welcome message';

export async function recipe(ctx: RecipeContext): Promise<RecipeOutput> {
const { cleanup } = ctx;

const sdk = new RunloopSDK({
bearerToken: process.env['RUNLOOP_API_KEY'],
});

// Create a devbox
const dbxOriginal = await sdk.devbox.create({
name: 'dbx_original',
launch_parameters: {
resource_size_request: 'X_SMALL',
keep_alive_time_seconds: 60 * 5,
},
});
cleanup.add(`devbox:${dbxOriginal.id}`, () => sdk.devbox.fromId(dbxOriginal.id).shutdown());

// Write a file to the original devbox
await dbxOriginal.file.write({ file_path: FILE_PATH, contents: ORIGINAL_CONTENT });

// Read and display the file contents
const catOriginalBefore = await dbxOriginal.cmd.exec(`cat ${FILE_PATH}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

catExecResult or something

const originalContentBefore = await catOriginalBefore.stdout();

// Create a disk snapshot of the original devbox
const snapshot = await dbxOriginal.snapshotDisk({
name: 'my-snapshot',
});
cleanup.add(`snapshot:${snapshot.id}`, () => snapshot.delete());

// Create a new devbox from the snapshot
const dbxClone = await sdk.devbox.createFromSnapshot(snapshot.id, {
name: 'dbx_clone',
launch_parameters: {
resource_size_request: 'X_SMALL',
keep_alive_time_seconds: 60 * 5,
},
});
cleanup.add(`devbox:${dbxClone.id}`, () => sdk.devbox.fromId(dbxClone.id).shutdown());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.addResource(x) we know which one it is with just the id xxx_yyy


// Modify the file on the original devbox
await dbxOriginal.file.write({ file_path: FILE_PATH, contents: MODIFIED_CONTENT });

// Read the file contents from both devboxes
const catClone = await dbxClone.cmd.exec(`cat ${FILE_PATH}`);
const cloneContent = await catClone.stdout();

const catOriginalAfter = await dbxOriginal.cmd.exec(`cat ${FILE_PATH}`);
const originalContentAfter = await catOriginalAfter.stdout();

return {
resourcesCreated: [`devbox:${dbxOriginal.id}`, `snapshot:${snapshot.id}`, `devbox:${dbxClone.id}`],
checks: [
{
name: 'original devbox file created successfully',
passed: catOriginalBefore.exitCode === 0 && originalContentBefore.trim() === ORIGINAL_CONTENT,
details: `content="${originalContentBefore.trim()}"`,
},
{
name: 'snapshot created successfully',
passed: Boolean(snapshot.id),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird for an example maybe we lift this out of the example itself?

details: `snapshotId=${snapshot.id}`,
},
{
name: 'clone devbox created from snapshot',
passed: Boolean(dbxClone.id),
details: `cloneId=${dbxClone.id}`,
},
{
name: 'clone has original file content (before modification)',
passed: catClone.exitCode === 0 && cloneContent.trim() === ORIGINAL_CONTENT,
details: `cloneContent="${cloneContent.trim()}"`,
},
{
name: 'original devbox has modified content',
passed: catOriginalAfter.exitCode === 0 && originalContentAfter.trim() === MODIFIED_CONTENT,
details: `originalContent="${originalContentAfter.trim()}"`,
},
{
name: 'clone and original have divergent state',
passed: cloneContent.trim() !== originalContentAfter.trim(),
details: `clone="${cloneContent.trim()}" vs original="${originalContentAfter.trim()}"`,
},
],
};
}

export const runDevboxSnapshotResumeExample = wrapRecipe({ recipe });

if (require.main === module) {
void runAsCli(runDevboxSnapshotResumeExample);
}
8 changes: 8 additions & 0 deletions examples/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import type { ExampleResult } from './types';
import { runDevboxFromBlueprintLifecycleExample } from './devbox-from-blueprint-lifecycle';
import { runDevboxSnapshotResumeExample } from './devbox-snapshot-resume';
import { runMcpGithubToolsExample } from './mcp-github-tools';

export interface ExampleRegistryEntry {
Expand All @@ -22,6 +23,13 @@ export const exampleRegistry: ExampleRegistryEntry[] = [
requiredEnv: ['RUNLOOP_API_KEY'],
run: runDevboxFromBlueprintLifecycleExample,
},
{
slug: 'devbox-snapshot-resume',
title: 'Devbox Snapshot and Resume',
fileName: 'devbox-snapshot-resume.ts',
requiredEnv: ['RUNLOOP_API_KEY'],
run: runDevboxSnapshotResumeExample,
},
{
slug: 'mcp-github-tools',
title: 'MCP Hub + Claude Code + GitHub',
Expand Down
1 change: 1 addition & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
## Core Patterns

- [Devbox lifecycle example](examples/devbox-from-blueprint-lifecycle.ts): Create blueprint, launch devbox, run commands, cleanup
- [Devbox snapshot and resume example](examples/devbox-snapshot-resume.ts): Snapshot disk, resume from snapshot, verify state isolation
- [MCP GitHub example](examples/mcp-github-tools.ts): MCP Hub integration with Claude Code

## API Reference
Expand Down