diff --git a/EXAMPLES.md b/EXAMPLES.md index 112a03d5a..a403f7db7 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -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) @@ -39,6 +40,37 @@ yarn test:examples **Source:** [`examples/devbox-from-blueprint-lifecycle.ts`](./examples/devbox-from-blueprint-lifecycle.ts) + +## 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) + ## MCP Hub + Claude Code + GitHub diff --git a/examples/devbox-snapshot-resume.ts b/examples/devbox-snapshot-resume.ts new file mode 100644 index 000000000..b437ebcfe --- /dev/null +++ b/examples/devbox-snapshot-resume.ts @@ -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 { + 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}`); + 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()); + + // 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), + 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); +} diff --git a/examples/registry.ts b/examples/registry.ts index ab58c4638..e14f0ba5f 100644 --- a/examples/registry.ts +++ b/examples/registry.ts @@ -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 { @@ -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', diff --git a/llms.txt b/llms.txt index f50958633..d09e8bdd1 100644 --- a/llms.txt +++ b/llms.txt @@ -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