Skip to content
Merged
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
8 changes: 5 additions & 3 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ Runnable examples live in [`examples/`](./examples).
<a id="devbox-from-blueprint-lifecycle"></a>
## Devbox From Blueprint (Run Command, Shutdown)

**Use case:** Create a devbox from a blueprint, run a command, validate output, and cleanly tear everything down.
**Use case:** Create a devbox from a blueprint, run a command, fetch logs, validate output, and cleanly tear everything down.

**Tags:** `devbox`, `blueprint`, `commands`, `cleanup`
**Tags:** `devbox`, `blueprint`, `commands`, `logs`, `cleanup`

### Workflow
- Create a blueprint
- Fetch blueprint build logs
- Create a devbox from the blueprint
- Execute a command in the devbox
- Validate exit code and stdout
- Fetch devbox logs
- Validate exit code, stdout, and logs
- Shutdown devbox and delete blueprint

### Prerequisites
Expand Down
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ const sdk = new RunloopSDK({
// Create a new devbox and wait for it to be ready
const devbox = await sdk.devbox.create();

// Execute a synchronous command
// Execute a command that returns immediately - exec blocks until completion
const result = await devbox.cmd.exec('echo "Hello, World!"');
console.log('Output:', await result.stdout()); // "Hello, World!"
console.log('Exit code:', result.exitCode); // 0

// Start a long-running HTTP server asynchronously
// Start a long-running HTTP server - execAsync returns immediately
const serverExec = await devbox.cmd.execAsync('npx http-server -p 8080');
console.log(`Started server with execution ID: ${serverExec.executionId}`);

Expand All @@ -42,6 +42,10 @@ console.log('Server status:', state.status); // "running"
// Later... kill the server when done
await serverExec.kill();

// Retrieve devbox logs
const logs = await devbox.logs();
console.log('Devbox logs:', logs.logs);

await devbox.shutdown();
```

Expand Down Expand Up @@ -85,6 +89,25 @@ The SDK provides object-oriented interfaces for all major Runloop resources:

The SDK is fully typed with comprehensive TypeScript definitions:

### Devbox Logs

Retrieve logs from a devbox, optionally filtered by execution ID or shell name:

```typescript
const devbox = await runloop.devbox.create();

// Get all devbox logs
const logs = await devbox.logs();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this all like executions too?

console.log(logs.logs);

// Filter logs by execution ID
const result = await devbox.cmd.exec('echo "hello"');
const execLogs = await devbox.logs({ execution_id: result.executionId });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this the smae as getting the asyncExecResult.logs?


// Filter logs by shell name
const shellLogs = await devbox.logs({ shell_name: 'my-shell' });
```

### Blueprints

Blueprints define reusable devbox configurations. Create blueprints via `runloop.blueprint.create()` and access build logs with `blueprint.logs()`:
Expand Down Expand Up @@ -341,6 +364,17 @@ The following runtimes are supported:

If you are interested in other runtime environments, please open or upvote an issue on GitHub.

## Development

After cloning the repository, run the bootstrap script and install git hooks:

```sh
./scripts/bootstrap
./scripts/install-hooks
```

This installs pre-push hooks that run linting and verify generated files are up to date.

## Contributing

See [the contributing documentation](./CONTRIBUTING.md).
23 changes: 21 additions & 2 deletions examples/devbox-from-blueprint-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
---
title: Devbox From Blueprint (Run Command, Shutdown)
slug: devbox-from-blueprint-lifecycle
use_case: Create a devbox from a blueprint, run a command, validate output, and cleanly tear everything down.
use_case: Create a devbox from a blueprint, run a command, fetch logs, validate output, and cleanly tear everything down.
workflow:
- Create a blueprint
- Fetch blueprint build logs
- Create a devbox from the blueprint
- Execute a command in the devbox
- Validate exit code and stdout
- Fetch devbox logs
- Validate exit code, stdout, and logs
- Shutdown devbox and delete blueprint
tags:
- devbox
- blueprint
- commands
- logs
- cleanup
prerequisites:
- RUNLOOP_API_KEY
Expand Down Expand Up @@ -50,6 +53,9 @@ export async function recipe(ctx: RecipeContext): Promise<RecipeOutput> {
);
cleanup.add(`blueprint:${blueprint.id}`, () => sdk.blueprint.fromId(blueprint.id).delete());

// Fetch blueprint build logs
const blueprintLogs = await blueprint.logs();

// Create a devbox from the blueprint
// Resource sizes: X_SMALL, SMALL, MEDIUM, LARGE, X_LARGE, XX_LARGE, CUSTOM_SIZE
const devbox = await sdk.devbox.createFromBlueprintId(blueprint.id, {
Expand All @@ -65,6 +71,9 @@ export async function recipe(ctx: RecipeContext): Promise<RecipeOutput> {
const result = await devbox.cmd.exec('echo "Hello from your devbox"');
const stdout = await result.stdout();

// Fetch devbox logs
const devboxLogs = await devbox.logs();

return {
resourcesCreated: [`blueprint:${blueprint.id}`, `devbox:${devbox.id}`],
checks: [
Expand All @@ -78,6 +87,16 @@ export async function recipe(ctx: RecipeContext): Promise<RecipeOutput> {
passed: stdout.includes('Hello from your devbox'),
details: stdout.trim(),
},
{
name: 'blueprint build logs are retrievable',
passed: blueprintLogs !== null && Array.isArray(blueprintLogs.logs),
details: `blueprintLogCount=${blueprintLogs.logs.length}`,
},
{
name: 'devbox logs are retrievable',
passed: devboxLogs !== null && Array.isArray(devboxLogs.logs),
details: `devboxLogCount=${devboxLogs.logs.length}`,
},
],
};
}
Expand Down
4 changes: 2 additions & 2 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
- Prefer SDK methods (`sdk.devbox`, `sdk.blueprint`, etc.) for object-oriented patterns and convenience
- For resources without SDK coverage (e.g., secrets, benchmarks), use `sdk.api.*` as a fallback
- Use `sdk.devbox.create()` to create devboxes; they auto-await readiness
- Use `devbox.cmd.exec('command')` for most commands—blocks until completion, returns `ExecutionResult` with stdout/stderr
- Use `devbox.cmd.execAsync('command')` for long-running or background processes (servers, watchers)—returns immediately with `Execution` handle to check status, get result, or kill
- Use `devbox.cmd.exec('command')` for commands expected to return immediately (e.g., `echo`, `pwd`, `cat`)—blocks until completion, returns `ExecutionResult` with stdout/stderr
- Use `devbox.cmd.execAsync('command')` for long-running or background processes (servers, watchers, builds)—returns immediately with `Execution` handle to check status, get result, or kill
- Both `exec` and `execAsync` support streaming callbacks (`stdout`, `stderr`, `output`) for real-time output
- Call `devbox.shutdown()` to clean up resources that are no longer in use.
- In example files, focus on the `recipe` function body for SDK usage patterns; ignore test harness files in the examples folder (`_harness.ts`, `registry.ts`, `types.ts`)
Expand Down
27 changes: 27 additions & 0 deletions scripts/install-hooks
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash

set -e

cd "$(dirname "$0")/.."

echo "==> Installing git hooks..."

mkdir -p .git/hooks

cat > .git/hooks/pre-push << 'EOF'
#!/usr/bin/env bash
set -e
cd "$(git rev-parse --show-toplevel)"

echo "==> Running lint checks..."
./scripts/lint

echo "==> Checking EXAMPLES.md is up to date..."
yarn check:examples-md

echo "==> All checks passed!"
EOF

chmod +x .git/hooks/pre-push

echo "==> Git hooks installed successfully!"
27 changes: 27 additions & 0 deletions src/sdk/devbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
DevboxKeepAliveResponse,
TunnelView,
} from '../resources/devboxes/devboxes';
import type { DevboxLogsListView, LogListParams } from '../resources/devboxes/logs';
import { PollingOptions } from '../lib/polling';
import { Snapshot } from './snapshot';
import { Execution } from './execution';
Expand Down Expand Up @@ -871,6 +872,32 @@ export class Devbox {
return `https://${port}-${tunnel.tunnel_key}.tunnel.runloop.ai`;
}

/**
* Get all logs from a running or completed devbox.
* Optionally filter by execution ID or shell name.
*
* @example
* ```typescript
* const logs = await devbox.logs();
* for (const log of logs.logs) {
* console.log(`[${log.level}] ${log.message}`);
* }
*
* // Filter by execution ID
* const execLogs = await devbox.logs({ execution_id: 'exec-123' });
*
* // Filter by shell name
* const shellLogs = await devbox.logs({ shell_name: 'my-shell' });
* ```
*
* @param {LogListParams} [params] - Optional filter parameters (execution_id, shell_name)
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxLogsListView>} The devbox logs
*/
async logs(params?: LogListParams, options?: Core.RequestOptions): Promise<DevboxLogsListView> {
return this.client.devboxes.logs.list(this._id, params, options);
}

/**
* Wait for the devbox to reach the running state.
* Uses optimized server-side polling for better performance.
Expand Down
61 changes: 61 additions & 0 deletions tests/smoketests/object-oriented/devbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1134,4 +1134,65 @@ describe('smoketest: object-oriented devbox', () => {
expect(output).toContain('test');
});
});

describe('devbox logs', () => {
let devbox: Devbox;

beforeAll(async () => {
devbox = await sdk.devbox.create({
name: uniqueName('sdk-devbox-logs'),
launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 },
});
}, SHORT_TIMEOUT);

afterAll(async () => {
if (devbox) {
await devbox.shutdown();
}
});

test('logs - basic retrieval', async () => {
expect(devbox).toBeDefined();

// Fetch logs - verifies API returns valid response structure
// Logs may be empty depending on timing
const logs = await devbox.logs();

expect(logs).toBeDefined();
expect(logs.logs).toBeDefined();
expect(Array.isArray(logs.logs)).toBe(true);
});

test('logs - with execution_id filter', async () => {
expect(devbox).toBeDefined();

// Run a command and get its execution ID
const execution = await devbox.cmd.execAsync('echo "filtered log test"');
const result = await execution.result();
expect(result.exitCode).toBe(0);

// Fetch logs filtered by execution ID - verifies API accepts the filter
const logs = await devbox.logs({ execution_id: execution.executionId });

expect(logs).toBeDefined();
expect(Array.isArray(logs.logs)).toBe(true);
});

test('logs - with shell_name filter', async () => {
expect(devbox).toBeDefined();

const shellName = 'test-logs-shell';
const shell = devbox.shell(shellName);

// Run a command in the named shell
const result = await shell.exec('echo "shell log test"');
expect(result.exitCode).toBe(0);

// Fetch logs filtered by shell name - verifies API accepts the filter
const logs = await devbox.logs({ shell_name: shellName });

expect(logs).toBeDefined();
expect(Array.isArray(logs.logs)).toBe(true);
});
});
});
Loading