Skip to content

Commit 0462122

Browse files
authored
fix: add logs to devboxes, smoke tests & examples (#742)
1 parent 4d9624c commit 0462122

7 files changed

Lines changed: 179 additions & 9 deletions

File tree

EXAMPLES.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ Runnable examples live in [`examples/`](./examples).
1313
<a id="devbox-from-blueprint-lifecycle"></a>
1414
## Devbox From Blueprint (Run Command, Shutdown)
1515

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

18-
**Tags:** `devbox`, `blueprint`, `commands`, `cleanup`
18+
**Tags:** `devbox`, `blueprint`, `commands`, `logs`, `cleanup`
1919

2020
### Workflow
2121
- Create a blueprint
22+
- Fetch blueprint build logs
2223
- Create a devbox from the blueprint
2324
- Execute a command in the devbox
24-
- Validate exit code and stdout
25+
- Fetch devbox logs
26+
- Validate exit code, stdout, and logs
2527
- Shutdown devbox and delete blueprint
2628

2729
### Prerequisites

README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ const sdk = new RunloopSDK({
2626
// Create a new devbox and wait for it to be ready
2727
const devbox = await sdk.devbox.create();
2828

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

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

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

45+
// Retrieve devbox logs
46+
const logs = await devbox.logs();
47+
console.log('Devbox logs:', logs.logs);
48+
4549
await devbox.shutdown();
4650
```
4751

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

8690
The SDK is fully typed with comprehensive TypeScript definitions:
8791

92+
### Devbox Logs
93+
94+
Retrieve logs from a devbox, optionally filtered by execution ID or shell name:
95+
96+
```typescript
97+
const devbox = await runloop.devbox.create();
98+
99+
// Get all devbox logs
100+
const logs = await devbox.logs();
101+
console.log(logs.logs);
102+
103+
// Filter logs by execution ID
104+
const result = await devbox.cmd.exec('echo "hello"');
105+
const execLogs = await devbox.logs({ execution_id: result.executionId });
106+
107+
// Filter logs by shell name
108+
const shellLogs = await devbox.logs({ shell_name: 'my-shell' });
109+
```
110+
88111
### Blueprints
89112

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

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

367+
## Development
368+
369+
After cloning the repository, run the bootstrap script and install git hooks:
370+
371+
```sh
372+
./scripts/bootstrap
373+
./scripts/install-hooks
374+
```
375+
376+
This installs pre-push hooks that run linting and verify generated files are up to date.
377+
344378
## Contributing
345379

346380
See [the contributing documentation](./CONTRIBUTING.md).

examples/devbox-from-blueprint-lifecycle.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44
---
55
title: Devbox From Blueprint (Run Command, Shutdown)
66
slug: devbox-from-blueprint-lifecycle
7-
use_case: Create a devbox from a blueprint, run a command, validate output, and cleanly tear everything down.
7+
use_case: Create a devbox from a blueprint, run a command, fetch logs, validate output, and cleanly tear everything down.
88
workflow:
99
- Create a blueprint
10+
- Fetch blueprint build logs
1011
- Create a devbox from the blueprint
1112
- Execute a command in the devbox
12-
- Validate exit code and stdout
13+
- Fetch devbox logs
14+
- Validate exit code, stdout, and logs
1315
- Shutdown devbox and delete blueprint
1416
tags:
1517
- devbox
1618
- blueprint
1719
- commands
20+
- logs
1821
- cleanup
1922
prerequisites:
2023
- RUNLOOP_API_KEY
@@ -50,6 +53,9 @@ export async function recipe(ctx: RecipeContext): Promise<RecipeOutput> {
5053
);
5154
cleanup.add(`blueprint:${blueprint.id}`, () => sdk.blueprint.fromId(blueprint.id).delete());
5255

56+
// Fetch blueprint build logs
57+
const blueprintLogs = await blueprint.logs();
58+
5359
// Create a devbox from the blueprint
5460
// Resource sizes: X_SMALL, SMALL, MEDIUM, LARGE, X_LARGE, XX_LARGE, CUSTOM_SIZE
5561
const devbox = await sdk.devbox.createFromBlueprintId(blueprint.id, {
@@ -65,6 +71,9 @@ export async function recipe(ctx: RecipeContext): Promise<RecipeOutput> {
6571
const result = await devbox.cmd.exec('echo "Hello from your devbox"');
6672
const stdout = await result.stdout();
6773

74+
// Fetch devbox logs
75+
const devboxLogs = await devbox.logs();
76+
6877
return {
6978
resourcesCreated: [`blueprint:${blueprint.id}`, `devbox:${devbox.id}`],
7079
checks: [
@@ -78,6 +87,16 @@ export async function recipe(ctx: RecipeContext): Promise<RecipeOutput> {
7887
passed: stdout.includes('Hello from your devbox'),
7988
details: stdout.trim(),
8089
},
90+
{
91+
name: 'blueprint build logs are retrievable',
92+
passed: blueprintLogs !== null && Array.isArray(blueprintLogs.logs),
93+
details: `blueprintLogCount=${blueprintLogs.logs.length}`,
94+
},
95+
{
96+
name: 'devbox logs are retrievable',
97+
passed: devboxLogs !== null && Array.isArray(devboxLogs.logs),
98+
details: `devboxLogCount=${devboxLogs.logs.length}`,
99+
},
81100
],
82101
};
83102
}

llms.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
- Prefer SDK methods (`sdk.devbox`, `sdk.blueprint`, etc.) for object-oriented patterns and convenience
2424
- For resources without SDK coverage (e.g., secrets, benchmarks), use `sdk.api.*` as a fallback
2525
- Use `sdk.devbox.create()` to create devboxes; they auto-await readiness
26-
- Use `devbox.cmd.exec('command')` for most commands—blocks until completion, returns `ExecutionResult` with stdout/stderr
27-
- 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
26+
- Use `devbox.cmd.exec('command')` for commands expected to return immediately (e.g., `echo`, `pwd`, `cat`)—blocks until completion, returns `ExecutionResult` with stdout/stderr
27+
- 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
2828
- Both `exec` and `execAsync` support streaming callbacks (`stdout`, `stderr`, `output`) for real-time output
2929
- Call `devbox.shutdown()` to clean up resources that are no longer in use.
3030
- 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`)

scripts/install-hooks

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
3+
set -e
4+
5+
cd "$(dirname "$0")/.."
6+
7+
echo "==> Installing git hooks..."
8+
9+
mkdir -p .git/hooks
10+
11+
cat > .git/hooks/pre-push << 'EOF'
12+
#!/usr/bin/env bash
13+
set -e
14+
cd "$(git rev-parse --show-toplevel)"
15+
16+
echo "==> Running lint checks..."
17+
./scripts/lint
18+
19+
echo "==> Checking EXAMPLES.md is up to date..."
20+
yarn check:examples-md
21+
22+
echo "==> All checks passed!"
23+
EOF
24+
25+
chmod +x .git/hooks/pre-push
26+
27+
echo "==> Git hooks installed successfully!"

src/sdk/devbox.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
DevboxKeepAliveResponse,
2020
TunnelView,
2121
} from '../resources/devboxes/devboxes';
22+
import type { DevboxLogsListView, LogListParams } from '../resources/devboxes/logs';
2223
import { PollingOptions } from '../lib/polling';
2324
import { Snapshot } from './snapshot';
2425
import { Execution } from './execution';
@@ -871,6 +872,32 @@ export class Devbox {
871872
return `https://${port}-${tunnel.tunnel_key}.tunnel.runloop.ai`;
872873
}
873874

875+
/**
876+
* Get all logs from a running or completed devbox.
877+
* Optionally filter by execution ID or shell name.
878+
*
879+
* @example
880+
* ```typescript
881+
* const logs = await devbox.logs();
882+
* for (const log of logs.logs) {
883+
* console.log(`[${log.level}] ${log.message}`);
884+
* }
885+
*
886+
* // Filter by execution ID
887+
* const execLogs = await devbox.logs({ execution_id: 'exec-123' });
888+
*
889+
* // Filter by shell name
890+
* const shellLogs = await devbox.logs({ shell_name: 'my-shell' });
891+
* ```
892+
*
893+
* @param {LogListParams} [params] - Optional filter parameters (execution_id, shell_name)
894+
* @param {Core.RequestOptions} [options] - Request options
895+
* @returns {Promise<DevboxLogsListView>} The devbox logs
896+
*/
897+
async logs(params?: LogListParams, options?: Core.RequestOptions): Promise<DevboxLogsListView> {
898+
return this.client.devboxes.logs.list(this._id, params, options);
899+
}
900+
874901
/**
875902
* Wait for the devbox to reach the running state.
876903
* Uses optimized server-side polling for better performance.

tests/smoketests/object-oriented/devbox.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,4 +1134,65 @@ describe('smoketest: object-oriented devbox', () => {
11341134
expect(output).toContain('test');
11351135
});
11361136
});
1137+
1138+
describe('devbox logs', () => {
1139+
let devbox: Devbox;
1140+
1141+
beforeAll(async () => {
1142+
devbox = await sdk.devbox.create({
1143+
name: uniqueName('sdk-devbox-logs'),
1144+
launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 },
1145+
});
1146+
}, SHORT_TIMEOUT);
1147+
1148+
afterAll(async () => {
1149+
if (devbox) {
1150+
await devbox.shutdown();
1151+
}
1152+
});
1153+
1154+
test('logs - basic retrieval', async () => {
1155+
expect(devbox).toBeDefined();
1156+
1157+
// Fetch logs - verifies API returns valid response structure
1158+
// Logs may be empty depending on timing
1159+
const logs = await devbox.logs();
1160+
1161+
expect(logs).toBeDefined();
1162+
expect(logs.logs).toBeDefined();
1163+
expect(Array.isArray(logs.logs)).toBe(true);
1164+
});
1165+
1166+
test('logs - with execution_id filter', async () => {
1167+
expect(devbox).toBeDefined();
1168+
1169+
// Run a command and get its execution ID
1170+
const execution = await devbox.cmd.execAsync('echo "filtered log test"');
1171+
const result = await execution.result();
1172+
expect(result.exitCode).toBe(0);
1173+
1174+
// Fetch logs filtered by execution ID - verifies API accepts the filter
1175+
const logs = await devbox.logs({ execution_id: execution.executionId });
1176+
1177+
expect(logs).toBeDefined();
1178+
expect(Array.isArray(logs.logs)).toBe(true);
1179+
});
1180+
1181+
test('logs - with shell_name filter', async () => {
1182+
expect(devbox).toBeDefined();
1183+
1184+
const shellName = 'test-logs-shell';
1185+
const shell = devbox.shell(shellName);
1186+
1187+
// Run a command in the named shell
1188+
const result = await shell.exec('echo "shell log test"');
1189+
expect(result.exitCode).toBe(0);
1190+
1191+
// Fetch logs filtered by shell name - verifies API accepts the filter
1192+
const logs = await devbox.logs({ shell_name: shellName });
1193+
1194+
expect(logs).toBeDefined();
1195+
expect(Array.isArray(logs.logs)).toBe(true);
1196+
});
1197+
});
11371198
});

0 commit comments

Comments
 (0)