Skip to content

Commit 9edd07f

Browse files
authored
!feat(devbox): devbox exec command is now a parameter for more convenient usage (#663)
* cp dines * cp dines
1 parent ac72bd7 commit 9edd07f

10 files changed

Lines changed: 62 additions & 95 deletions

File tree

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,12 @@ const sdk = new RunloopSDK({
2929
const devbox = await sdk.devbox.create();
3030

3131
// Execute a synchronous command
32-
const result = await devbox.cmd.exec({ command: 'echo "Hello, World!"' });
32+
const result = await devbox.cmd.exec('echo "Hello, World!"');
3333
console.log('Output:', await result.stdout()); // "Hello, World!"
3434
console.log('Exit code:', result.exitCode); // 0
3535

3636
// Start a long-running HTTP server asynchronously
37-
const serverExec = await devbox.cmd.execAsync({
38-
command: 'npx http-server -p 8080',
39-
});
37+
const serverExec = await devbox.cmd.execAsync('npx http-server -p 8080');
4038
console.log(`Started server with execution ID: ${serverExec.executionId}`);
4139

4240
// Check server status
@@ -104,7 +102,7 @@ const runloop = new Runloop()
104102

105103
const devboxResult = await runloop.devboxes.createAndAwaitRunning()
106104

107-
await runloop.devboxes.executeAndAwaitCompletion(devboxResult.id, {"command": "touch example.txt"})
105+
await runloop.devboxes.executeAndAwaitCompletion(devboxResult.id, { command: "touch example.txt" })
108106

109107
const snapshotResult = await runloop.devbox.snapshotDisk()
110108
await runloop.devboxes.snapshotDisk(devboxResult.id)
@@ -118,7 +116,7 @@ import { RunloopSDK } from '@runloop/api-client';
118116
const runloop = new RunloopSDK();
119117

120118
const devbox = await runloop.devbox.create();
121-
await devbox.cmd.exec({ command: "touch example.txt" });
119+
await devbox.cmd.exec("touch example.txt");
122120
const snapshot = await devbox.snapshotDisk();
123121
await snapshot.createDevbox();
124122
...
@@ -144,7 +142,7 @@ The SDK provides comprehensive error handling with typed exceptions:
144142
```typescript
145143
try {
146144
const devbox = await runloop.devbox.create();
147-
const result = await devbox.cmd.exec({ command: 'invalid-command' });
145+
const result = await devbox.cmd.exec('invalid-command');
148146
} catch (error) {
149147
if (error instanceof RunloopSDK.APIError) {
150148
console.log('API Error:', error.status, error.message);

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ export interface ClientOptions {
246246
* import { RunloopSDK } from '@runloop/api-client';
247247
* const runloop = new RunloopSDK();
248248
* const devbox = await runloop.devbox.create();
249-
* const result = await devbox.cmd.exec({ command: 'echo "Hello, World!"' });
249+
* const result = await devbox.cmd.exec('echo "Hello, World!"');
250250
* console.log(result.exitCode);
251251
* ```
252252
*/

src/sdk.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type ContentType = ObjectCreateParams['content_type'];
3939
* ```typescript
4040
* const runloop = new RunloopSDK(); // export RUNLOOP_API_KEY will automatically be used.
4141
* const devbox = await runloop.devbox.create({ name: 'my-devbox' });
42-
* const result = await devbox.cmd.exec({ command: 'echo "Hello, World!"' });
42+
* const result = await devbox.cmd.exec('echo "Hello, World!"');
4343
* console.log(result.exitCode);
4444
* ```
4545
*
@@ -141,7 +141,7 @@ export class RunloopSDK {
141141
* ```typescript
142142
* const runloop = new RunloopSDK();
143143
* const devbox = await runloop.devbox.create({ name: 'my-devbox' });
144-
* const result = await devbox.cmd.exec({ command: 'echo "Hello, World!"' });
144+
* const result = await devbox.cmd.exec('echo "Hello, World!"');
145145
* ```
146146
*/
147147
export class DevboxOps {
@@ -162,7 +162,7 @@ export class DevboxOps {
162162
* const runloop = new RunloopSDK();
163163
* const devbox = await runloop.devbox.create({ name: 'my-devbox' });
164164
*
165-
* devbox.cmd.exec({ command: 'echo "Hello, World!"' });
165+
* devbox.cmd.exec('echo "Hello, World!"');
166166
* ...
167167
* ```
168168
*

src/sdk/devbox.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -155,30 +155,32 @@ export class DevboxCmdOps {
155155
* @example
156156
* ```typescript
157157
* // Simple execution
158-
* const result = await devbox.cmd.exec({ command: 'ls -la' });
158+
* const result = await devbox.cmd.exec('ls -la');
159159
* console.log(await result.stdout());
160160
*
161161
* // With streaming callbacks
162-
* const result = await devbox.cmd.exec({
163-
* command: 'npm install',
162+
* const result = await devbox.cmd.exec('npm install', {
164163
* stdout: (line) => process.stdout.write(line),
165164
* stderr: (line) => process.stderr.write(line),
166165
* });
167166
* ```
168167
*
169-
* @param {DevboxExecuteParams & ExecuteStreamingCallbacks} params - Parameters containing the command, optional shell name, and optional callbacks
168+
* @param {string} command - The command to execute
169+
* @param {Omit<DevboxExecuteParams, 'command'> & ExecuteStreamingCallbacks} [params] - Optional parameters including shell name and callbacks
170170
* @param {Core.RequestOptions & { polling?: Partial<PollingOptions<DevboxAsyncExecutionDetailView>> }} [options] - Request options with optional polling configuration
171171
* @returns {Promise<ExecutionResult>} {@link ExecutionResult} with stdout, stderr, and exit status
172172
*/
173173
async exec(
174-
params: DevboxExecuteParams & ExecuteStreamingCallbacks,
174+
command: string,
175+
params?: Omit<DevboxExecuteParams, 'command'> & ExecuteStreamingCallbacks,
175176
options?: Core.RequestOptions & { polling?: Partial<PollingOptions<DevboxAsyncExecutionDetailView>> },
176177
): Promise<ExecutionResult> {
177-
const hasCallbacks = params.stdout || params.stderr || params.output;
178+
const fullParams = { ...params, command };
179+
const hasCallbacks = fullParams.stdout || fullParams.stderr || fullParams.output;
178180

179181
if (hasCallbacks) {
180182
// With callbacks: use async execution workflow to enable streaming
181-
const { stdout, stderr, output, ...executeParams } = params;
183+
const { stdout, stderr, output, ...executeParams } = fullParams;
182184
const execution = await this.client.devboxes.executeAsync(this.devboxId, executeParams, options);
183185

184186
// Start streaming and await both completion and streaming
@@ -204,7 +206,7 @@ export class DevboxCmdOps {
204206
return new ExecutionResult(this.client, this.devboxId, execution.execution_id, result);
205207
} else {
206208
// Without callbacks: use existing optimized workflow
207-
const result = await this.client.devboxes.executeAndAwaitCompletion(this.devboxId, params, options);
209+
const result = await this.client.devboxes.executeAndAwaitCompletion(this.devboxId, fullParams, options);
208210
return new ExecutionResult(this.client, this.devboxId, result.execution_id, result);
209211
}
210212
}
@@ -218,8 +220,7 @@ export class DevboxCmdOps {
218220
*
219221
* @example
220222
* ```typescript
221-
* const execution = await devbox.cmd.execAsync({
222-
* command: 'long-running-task.sh',
223+
* const execution = await devbox.cmd.execAsync('long-running-task.sh', {
223224
* stdout: (line) => console.log(`[LOG] ${line}`),
224225
* });
225226
*
@@ -231,15 +232,18 @@ export class DevboxCmdOps {
231232
* }
232233
* ```
233234
*
234-
* @param {DevboxExecuteAsyncParams & ExecuteStreamingCallbacks} params - Parameters containing the command, optional shell name, and optional callbacks
235+
* @param {string} command - The command to execute
236+
* @param {Omit<DevboxExecuteAsyncParams, 'command'> & ExecuteStreamingCallbacks} [params] - Optional parameters including shell name and callbacks
235237
* @param {Core.RequestOptions} [options] - Request options
236238
* @returns {Promise<Execution>} {@link Execution} object for tracking and controlling the command
237239
*/
238240
async execAsync(
239-
params: DevboxExecuteAsyncParams & ExecuteStreamingCallbacks,
241+
command: string,
242+
params?: Omit<DevboxExecuteAsyncParams, 'command'> & ExecuteStreamingCallbacks,
240243
options?: Core.RequestOptions,
241244
): Promise<Execution> {
242-
const { stdout, stderr, output, ...executeParams } = params;
245+
const fullParams = { ...params, command };
246+
const { stdout, stderr, output, ...executeParams } = fullParams;
243247
const execution = await this.client.devboxes.executeAsync(this.devboxId, executeParams, options);
244248

245249
// Start streaming in background if callbacks provided
@@ -359,7 +363,7 @@ export class DevboxFileOps {
359363
*
360364
* const runloop = new RunloopSDK();
361365
* const devbox = await runloop.devbox.create({ name: 'my-devbox' });
362-
* devbox.cmd.exec({ command: 'echo "Hello, World!"' });
366+
* devbox.cmd.exec('echo "Hello, World!"');
363367
* ...
364368
* ```
365369
*
@@ -403,7 +407,7 @@ export class Devbox {
403407
* const runloop = new RunloopSDK();
404408
* const devbox = await runloop.devbox.create({ name: 'my-devbox' });
405409
*
406-
* devbox.cmd.exec({ command: 'echo "Hello, World!"' });
410+
* devbox.cmd.exec('echo "Hello, World!"');
407411
* ...
408412
* ```
409413
*
@@ -715,6 +719,7 @@ export class Devbox {
715719
* @example
716720
* ```typescript
717721
* await devbox.suspend();
722+
* // Optionally, wait for the devbox to reach the suspended state
718723
* await devbox.awaitSuspended();
719724
* ```
720725
*

src/sdk/execution-result.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type { DevboxAsyncExecutionDetailView } from '../resources/devboxes/devbo
1919
* const devbox = runloop.devbox.fromId('devbox-123');
2020
*
2121
* // Get result from synchronous execution
22-
* const result = await devbox.cmd.exec({ command: 'ls -la' });
22+
* const result = await devbox.cmd.exec('ls -la');
2323
*
2424
* // Check success
2525
* if (result.success) {

src/sdk/execution.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ import { ExecutionResult } from './execution-result';
2323
* const devbox = runloop.devbox.fromId('devbox-123');
2424
*
2525
* // Start async execution with streaming
26-
* const execution = await devbox.cmd.execAsync({
27-
* command: 'npx http-server -p 8080',
26+
* const execution = await devbox.cmd.execAsync('npx http-server -p 8080', {
2827
* stdout: (line) => console.log(`[LOG] ${line}`),
2928
* stderr: (line) => console.error(`[ERROR] ${line}`),
3029
* });
@@ -86,7 +85,7 @@ export class Execution {
8685
* ```typescript
8786
* const runloop = new RunloopSDK();
8887
* const devbox = runloop.devbox.fromId('devbox-123');
89-
* const execution = await devbox.cmd.execAsync({ command: 'npm install' });
88+
* const execution = await devbox.cmd.execAsync('npm install');
9089
*
9190
* // Other work while command runs...
9291
*
@@ -133,7 +132,7 @@ export class Execution {
133132
*
134133
* @example
135134
* ```typescript
136-
* const execution = await devbox.cmd.execAsync({ command: 'npx http-server -p 8080' });
135+
* const execution = await devbox.cmd.execAsync('npx http-server -p 8080');
137136
* const state = await execution.getState();
138137
* console.log(`Status: ${state.status}`);
139138
* ```

tests/objects/devbox.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ describe('Devbox (New API)', () => {
120120

121121
mockClient.devboxes.executeAndAwaitCompletion.mockResolvedValue(mockExecution);
122122

123-
const result = await devbox.cmd.exec({ command: 'echo "Hello World"' });
123+
const result = await devbox.cmd.exec('echo "Hello World"');
124124

125125
expect(mockClient.devboxes.executeAndAwaitCompletion).toHaveBeenCalledWith(
126126
'devbox-123',
@@ -142,7 +142,7 @@ describe('Devbox (New API)', () => {
142142

143143
mockClient.devboxes.executeAsync.mockResolvedValue(mockExecution);
144144

145-
const result = await devbox.cmd.execAsync({ command: 'sleep 10' });
145+
const result = await devbox.cmd.execAsync('sleep 10');
146146

147147
expect(mockClient.devboxes.executeAsync).toHaveBeenCalledWith(
148148
'devbox-123',
@@ -288,7 +288,7 @@ describe('Devbox (New API)', () => {
288288
const error = new Error('Command failed');
289289
mockClient.devboxes.executeAndAwaitCompletion.mockRejectedValue(error);
290290

291-
await expect(devbox.cmd.exec({ command: 'failing-command' })).rejects.toThrow('Command failed');
291+
await expect(devbox.cmd.exec('failing-command')).rejects.toThrow('Command failed');
292292
});
293293
});
294294
});

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

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('smoketest: object-oriented devbox', () => {
3838

3939
test('execute synchronous command', async () => {
4040
expect(devbox).toBeDefined();
41-
const result = await devbox.cmd.exec({ command: 'echo "Hello from SDK!"' });
41+
const result = await devbox.cmd.exec('echo "Hello from SDK!"');
4242
expect(result).toBeDefined();
4343
expect(result.exitCode).toBe(0);
4444
const output = await result.stdout();
@@ -47,7 +47,7 @@ describe('smoketest: object-oriented devbox', () => {
4747

4848
test('execute asynchronous command', async () => {
4949
expect(devbox).toBeDefined();
50-
const execution = await devbox.cmd.execAsync({ command: 'sleep 2 && echo "Async command completed"' });
50+
const execution = await devbox.cmd.execAsync('sleep 2 && echo "Async command completed"');
5151
expect(execution).toBeDefined();
5252
expect(execution.executionId).toBeTruthy();
5353

@@ -298,8 +298,7 @@ describe('smoketest: object-oriented devbox', () => {
298298
test('exec with stdout callback', async () => {
299299
const stdoutLines: string[] = [];
300300

301-
const result = await devbox.cmd.exec({
302-
command: 'echo "line1" && echo "line2" && echo "line3"',
301+
const result = await devbox.cmd.exec('echo "line1" && echo "line2" && echo "line3"', {
303302
stdout: (line) => {
304303
stdoutLines.push(line);
305304
},
@@ -319,8 +318,7 @@ describe('smoketest: object-oriented devbox', () => {
319318
test('exec with stderr callback', async () => {
320319
const stderrLines: string[] = [];
321320

322-
const result = await devbox.cmd.exec({
323-
command: 'echo "error1" >&2 && echo "error2" >&2',
321+
const result = await devbox.cmd.exec('echo "error1" >&2 && echo "error2" >&2', {
324322
stderr: (line) => {
325323
stderrLines.push(line);
326324
},
@@ -339,8 +337,7 @@ describe('smoketest: object-oriented devbox', () => {
339337
test('exec with output callback (both stdout and stderr)', async () => {
340338
const allLines: string[] = [];
341339

342-
const result = await devbox.cmd.exec({
343-
command: 'echo "stdout1" && echo "stderr1" >&2 && echo "stdout2"',
340+
const result = await devbox.cmd.exec('echo "stdout1" && echo "stderr1" >&2 && echo "stdout2"', {
344341
output: (line) => {
345342
allLines.push(line);
346343
},
@@ -360,8 +357,7 @@ describe('smoketest: object-oriented devbox', () => {
360357
const stderrLines: string[] = [];
361358
const outputLines: string[] = [];
362359

363-
const result = await devbox.cmd.exec({
364-
command: 'echo "out1" && echo "err1" >&2 && echo "out2"',
360+
const result = await devbox.cmd.exec('echo "out1" && echo "err1" >&2 && echo "out2"', {
365361
stdout: (line) => stdoutLines.push(line),
366362
stderr: (line) => stderrLines.push(line),
367363
output: (line) => outputLines.push(line),
@@ -387,9 +383,7 @@ describe('smoketest: object-oriented devbox', () => {
387383
});
388384

389385
test('exec WITHOUT callbacks (preserve existing behavior)', async () => {
390-
const result = await devbox.cmd.exec({
391-
command: 'echo "test output"',
392-
});
386+
const result = await devbox.cmd.exec('echo "test output"');
393387

394388
expect(result.success).toBe(true);
395389
expect(result.exitCode).toBe(0);
@@ -402,8 +396,7 @@ describe('smoketest: object-oriented devbox', () => {
402396
let receivedBeforeCompletion = false;
403397

404398
// Start async execution with streaming
405-
const execution = await devbox.cmd.execAsync({
406-
command: 'echo "immediate" && sleep 2 && echo "delayed"',
399+
const execution = await devbox.cmd.execAsync('echo "immediate" && sleep 2 && echo "delayed"', {
407400
stdout: (line) => {
408401
stdoutLines.push(line);
409402
if (line.includes('immediate')) {
@@ -434,8 +427,7 @@ describe('smoketest: object-oriented devbox', () => {
434427
test('execAsync with stderr callback', async () => {
435428
const stderrLines: string[] = [];
436429

437-
const execution = await devbox.cmd.execAsync({
438-
command: 'echo "error output" >&2',
430+
const execution = await devbox.cmd.execAsync('echo "error output" >&2', {
439431
stderr: (line) => {
440432
stderrLines.push(line);
441433
},
@@ -455,8 +447,7 @@ describe('smoketest: object-oriented devbox', () => {
455447
const stdoutLines: string[] = [];
456448
const stderrLines: string[] = [];
457449

458-
const result = await devbox.cmd.exec({
459-
command: 'echo "to stdout" && echo "to stderr" >&2 && echo "more stdout"',
450+
const result = await devbox.cmd.exec('echo "to stdout" && echo "to stderr" >&2 && echo "more stdout"', {
460451
stdout: (line) => stdoutLines.push(line),
461452
stderr: (line) => stderrLines.push(line),
462453
});
@@ -480,8 +471,7 @@ describe('smoketest: object-oriented devbox', () => {
480471
test('exec with long output - verify all lines received', async () => {
481472
const stdoutLines: string[] = [];
482473

483-
const result = await devbox.cmd.exec({
484-
command: 'for i in {1..1000}; do echo "line $i"; done',
474+
const result = await devbox.cmd.exec('for i in {1..1000}; do echo "line $i"; done', {
485475
stdout: (line) => stdoutLines.push(line),
486476
});
487477

@@ -511,16 +501,14 @@ describe('smoketest: object-oriented devbox', () => {
511501
let taskBCount = 0;
512502

513503
// Start both executions at the same time (don't await)
514-
const executionA = devbox.cmd.execAsync({
515-
command: 'echo "A1" && sleep 0.5 && echo "A2" && sleep 0.5 && echo "A3"',
504+
const executionA = devbox.cmd.execAsync('echo "A1" && sleep 0.5 && echo "A2" && sleep 0.5 && echo "A3"', {
516505
stdout: (line) => {
517506
taskALogs.push(line);
518507
taskACount++;
519508
},
520509
});
521510

522-
const executionB = devbox.cmd.execAsync({
523-
command: 'sleep 0.3 && echo "B1" && sleep 0.5 && echo "B2" && sleep 0.5 && echo "B3"',
511+
const executionB = devbox.cmd.execAsync('sleep 0.3 && echo "B1" && sleep 0.5 && echo "B2" && sleep 0.5 && echo "B3"', {
524512
stdout: (line) => {
525513
taskBLogs.push(line);
526514
taskBCount++;

0 commit comments

Comments
 (0)