Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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, ORIGINAL_CONTENT);

Check failure on line 54 in examples/devbox-snapshot-resume.ts

View workflow job for this annotation

GitHub Actions / lint

Argument of type 'string' is not assignable to parameter of type 'DevboxWriteFileContentsParams'.

// 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, MODIFIED_CONTENT);

Check failure on line 77 in examples/devbox-snapshot-resume.ts

View workflow job for this annotation

GitHub Actions / lint

Argument of type 'string' is not assignable to parameter of type 'DevboxWriteFileContentsParams'.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: The calls to the devbox file write API are using the wrong argument shape (path, contents positional arguments) instead of the expected single params object, so at runtime the SDK will send an invalid request body to writeFileContents, likely causing the file not to be written and subsequent cat commands and checks to fail. [type error]

Severity Level: Major ⚠️
- ❌ Devbox snapshot/resume example script fails on first write.
- ⚠️ `yarn test:examples` will fail when this example runs.
- ⚠️ Documentation misleads about correct `devbox.file.write` usage.
Suggested change
await dbxOriginal.file.write(FILE_PATH, 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, MODIFIED_CONTENT);
await dbxOriginal.file.write({ file_path: FILE_PATH, contents: ORIGINAL_CONTENT });
await dbxOriginal.file.write({ file_path: FILE_PATH, contents: MODIFIED_CONTENT });
Steps of Reproduction ✅
1. From the repo root `/workspace/api-client-ts`, run the documented example command `yarn
tsn -T examples/devbox-snapshot-resume.ts` (as specified in the front‑matter of
`examples/devbox-snapshot-resume.ts:23`).

2. Execution enters `recipe()` in `examples/devbox-snapshot-resume.ts:36-41`, constructs a
`RunloopSDK` instance, and creates a devbox via `sdk.devbox.create(...)` at lines 43-50,
yielding `dbxOriginal`.

3. The script calls `await dbxOriginal.file.write(FILE_PATH, ORIGINAL_CONTENT);` at
`examples/devbox-snapshot-resume.ts:54`, passing two positional arguments (string path and
string contents). At runtime this invokes `DevboxFileOps.write(params, options?)` defined
in `src/sdk/devbox.ts:469-485`, where `params` is expected to be a
`DevboxWriteFileContentsParams` object but actually receives the string `FILE_PATH`, and
`options` receives the string `ORIGINAL_CONTENT`.

4. Inside `DevboxFileOps.write`, the call
`this.client.devboxes.writeFileContents(this.devboxId, params, options)` forwards these
incorrect arguments to the generated API client. The API contract is validated by tests in
`tests/api-resources/devboxes/devboxes.test.ts:577-595`, which show that
`writeFileContents` requires `client.devboxes.writeFileContents('id', { contents:
'contents', file_path: 'file_path' })`. Because the example passes a bare string instead
of the required params object, the HTTP request body is malformed, causing the write to
fail so `/home/user/welcome.txt` is never created and the example flow (subsequent `cat`
and checks at lines 57-84) fails when run.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** examples/devbox-snapshot-resume.ts
**Line:** 54:77
**Comment:**
	*Type Error: The calls to the devbox file write API are using the wrong argument shape (`path, contents` positional arguments) instead of the expected single params object, so at runtime the SDK will send an invalid request body to `writeFileContents`, likely causing the file not to be written and subsequent `cat` commands and checks to fail.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎


// 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
Loading