From f5c3d9bdc1b5f509c0acc65807e6b1ba93445566 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 25 Nov 2025 16:05:04 -0800 Subject: [PATCH 1/5] cp dines --- src/sdk/devbox.ts | 207 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/src/sdk/devbox.ts b/src/sdk/devbox.ts index e8338ddb9..16e80af37 100644 --- a/src/sdk/devbox.ts +++ b/src/sdk/devbox.ts @@ -257,6 +257,175 @@ export class DevboxCmdOps { } } +/** + * Named shell operations for a devbox. + * Provides methods for executing commands in a persistent, stateful shell session. + * + * Named shells are stateful and maintain environment variables and the current working directory (CWD) + * across commands, just like a real shell on your local computer. Commands executed through the same + * named shell instance will execute sequentially - the shell can only run one command at a time with + * automatic queuing. This ensures that environment changes and directory changes from one command + * are preserved for the next command. + * + * @example + * ```typescript + * // Create a named shell + * const shell = devbox.shell('my-session'); + * + * // Commands execute sequentially and share state + * await shell.exec('cd /app'); + * await shell.exec('export MY_VAR=value'); + * await shell.exec('echo $MY_VAR'); // Will output 'value' because env is preserved + * await shell.exec('pwd'); // Will output '/app' because CWD is preserved + * ``` + */ +export class DevboxNamedShell { + /** + * @private + */ + constructor( + private client: Runloop, + private devboxId: string, + private shellName: string, + private startStreamingWithCallbacks: ( + executionId: string, + stdout?: (line: string) => void, + stderr?: (line: string) => void, + output?: (line: string) => void, + ) => Promise, + ) {} + + /** + * Execute a command in the named shell and wait for it to complete. + * Optionally provide callbacks to stream logs in real-time. + * + * The command will execute in the persistent shell session, maintaining environment variables + * and the current working directory from previous commands. Commands are queued and execute + * sequentially - only one command runs at a time in the named shell. + * + * When callbacks are provided, this method waits for both the command to complete + * AND all streaming data to be processed before returning. + * + * @example + * ```typescript + * const shell = devbox.shell('my-session'); + * + * // Simple execution + * const result = await shell.exec('ls -la'); + * console.log(await result.stdout()); + * + * // With streaming callbacks + * const result = await shell.exec('npm install', { + * stdout: (line) => process.stdout.write(line), + * stderr: (line) => process.stderr.write(line), + * }); + * + * // Stateful execution - environment and CWD are preserved + * await shell.exec('cd /app'); + * await shell.exec('export NODE_ENV=production'); + * const result = await shell.exec('npm start'); // Runs in /app with NODE_ENV=production + * ``` + * + * @param {string} command - The command to execute + * @param {Omit, 'shell_name'> & ExecuteStreamingCallbacks} [params] - Optional parameters (shell_name is automatically set) + * @param {Core.RequestOptions & { polling?: Partial> }} [options] - Request options with optional polling configuration + * @returns {Promise} {@link ExecutionResult} with stdout, stderr, and exit status + */ + async exec( + command: string, + params?: Omit, 'shell_name'> & ExecuteStreamingCallbacks, + options?: Core.RequestOptions & { polling?: Partial> }, + ): Promise { + const fullParams = { ...params, command, shell_name: this.shellName }; + const hasCallbacks = fullParams.stdout || fullParams.stderr || fullParams.output; + + if (hasCallbacks) { + // With callbacks: use async execution workflow to enable streaming + const { stdout, stderr, output, ...executeParams } = fullParams; + const execution = await this.client.devboxes.executeAsync(this.devboxId, executeParams, options); + + // Start streaming and await both completion and streaming + const streamingPromise = this.startStreamingWithCallbacks( + execution.execution_id, + stdout, + stderr, + output, + ); + + // Wait for both command completion and streaming to finish (using allSettled for robustness) + const results = await Promise.allSettled([ + this.client.devboxes.executions.awaitCompleted(this.devboxId, execution.execution_id, options), + streamingPromise, + ]); + + // Extract command result (throw if it failed, ignore streaming errors) + if (results[0].status === 'rejected') { + throw results[0].reason; + } + const result = results[0].value; + + return new ExecutionResult(this.client, this.devboxId, execution.execution_id, result); + } else { + // Without callbacks: use existing optimized workflow + const result = await this.client.devboxes.executeAndAwaitCompletion(this.devboxId, fullParams, options); + return new ExecutionResult(this.client, this.devboxId, result.execution_id, result); + } + } + + /** + * Execute a command in the named shell asynchronously without waiting for completion. + * Optionally provide callbacks to stream logs in real-time as they are produced. + * + * The command will execute in the persistent shell session, maintaining environment variables + * and the current working directory from previous commands. Commands are queued and execute + * sequentially - only one command runs at a time in the named shell. + * + * Callbacks fire in real-time as logs arrive. When you call execution.result(), + * it will wait for both the command to complete and all streaming to finish. + * + * @example + * ```typescript + * const shell = devbox.shell('my-session'); + * + * const execution = await shell.execAsync('long-running-task.sh', { + * stdout: (line) => console.log(`[LOG] ${line}`), + * }); + * + * // Do other work while command runs... + * // Note: if you call shell.exec() or shell.execAsync() again, it will queue + * // and wait for this command to complete first + * + * const result = await execution.result(); + * if (result.success) { + * console.log('Task completed successfully!'); + * } + * ``` + * + * @param {string} command - The command to execute + * @param {Omit, 'shell_name'> & ExecuteStreamingCallbacks} [params] - Optional parameters (shell_name is automatically set) + * @param {Core.RequestOptions} [options] - Request options + * @returns {Promise} {@link Execution} object for tracking and controlling the command + */ + async execAsync( + command: string, + params?: Omit, 'shell_name'> & ExecuteStreamingCallbacks, + options?: Core.RequestOptions, + ): Promise { + const fullParams = { ...params, command, shell_name: this.shellName }; + const { stdout, stderr, output, ...executeParams } = fullParams; + const execution = await this.client.devboxes.executeAsync(this.devboxId, executeParams, options); + + // Start streaming in background if callbacks provided + let streamingPromise: Promise | undefined; + if (stdout || stderr || output) { + // Start streaming - will be awaited when result() is called + streamingPromise = this.startStreamingWithCallbacks(execution.execution_id, stdout, stderr, output); + } + + return new Execution(this.client, this.devboxId, execution.execution_id, execution, streamingPromise); + } +} + /** * File operations for a devbox. * Provides methods for reading, writing, uploading, and downloading files. @@ -541,6 +710,39 @@ export class Devbox { return this._id; } + /** + * Create a named shell instance for stateful command execution. + * + * Named shells are stateful and maintain environment variables and the current working directory (CWD) + * across commands, just like a real shell on your local computer. Commands executed through the same + * named shell instance will execute sequentially - the shell can only run one command at a time with + * automatic queuing. This ensures that environment changes and directory changes from one command + * are preserved for the next command. + * + * @example + * ```typescript + * // Create a named shell + * const shell = devbox.shell('my-session'); + * + * // Commands execute sequentially and share state + * await shell.exec('cd /app'); + * await shell.exec('export MY_VAR=value'); + * await shell.exec('echo $MY_VAR'); // Will output 'value' because env is preserved + * await shell.exec('pwd'); // Will output '/app' because CWD is preserved + * ``` + * + * @param {string} shellName - The name of the persistent shell session + * @returns {DevboxNamedShell} A {@link DevboxNamedShell} instance for executing commands in the named shell + */ + shell(shellName: string): DevboxNamedShell { + return new DevboxNamedShell( + this.client, + this._id, + shellName, + this.startStreamingWithCallbacks.bind(this), + ); + } + /** * Start streaming logs with callbacks. * Returns a promise that resolves when all streams complete. @@ -799,5 +1001,10 @@ export declare namespace Devbox { * @see {@link ExecuteStreamingCallbacks} */ type ExecuteStreamingCallbacks as ExecuteStreamingCallbacks, + /** + * Named shell operations class for stateful command execution. + * @see {@link DevboxNamedShell} + */ + DevboxNamedShell as NamedShell, }; } From e189e280325f05d4adc0162cc1d1cc2144199a59 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 25 Nov 2025 16:09:06 -0800 Subject: [PATCH 2/5] cp dines --- .../smoketests/object-oriented/devbox.test.ts | 266 +++++++++++++++++- 1 file changed, 255 insertions(+), 11 deletions(-) diff --git a/tests/smoketests/object-oriented/devbox.test.ts b/tests/smoketests/object-oriented/devbox.test.ts index d5528e59c..772ffd387 100644 --- a/tests/smoketests/object-oriented/devbox.test.ts +++ b/tests/smoketests/object-oriented/devbox.test.ts @@ -501,19 +501,25 @@ describe('smoketest: object-oriented devbox', () => { let taskBCount = 0; // Start both executions at the same time (don't await) - const executionA = devbox.cmd.execAsync('echo "A1" && sleep 0.5 && echo "A2" && sleep 0.5 && echo "A3"', { - stdout: (line) => { - taskALogs.push(line); - taskACount++; + const executionA = devbox.cmd.execAsync( + 'echo "A1" && sleep 0.5 && echo "A2" && sleep 0.5 && echo "A3"', + { + stdout: (line) => { + taskALogs.push(line); + taskACount++; + }, }, - }); - - const executionB = devbox.cmd.execAsync('sleep 0.3 && echo "B1" && sleep 0.5 && echo "B2" && sleep 0.5 && echo "B3"', { - stdout: (line) => { - taskBLogs.push(line); - taskBCount++; + ); + + const executionB = devbox.cmd.execAsync( + 'sleep 0.3 && echo "B1" && sleep 0.5 && echo "B2" && sleep 0.5 && echo "B3"', + { + stdout: (line) => { + taskBLogs.push(line); + taskBCount++; + }, }, - }); + ); // Wait for both to start const [execA, execB] = await Promise.all([executionA, executionB]); @@ -554,4 +560,242 @@ describe('smoketest: object-oriented devbox', () => { expect(taskBCombined).toBe(await resultB.stdout()); }); }); + + describe('named shell - stateful command execution', () => { + let devbox: Devbox; + + beforeAll(async () => { + devbox = await sdk.devbox.create({ + name: uniqueName('sdk-devbox-named-shell'), + launch_parameters: { resource_size_request: 'X_SMALL', keep_alive_time_seconds: 60 * 5 }, + }); + }, THIRTY_SECOND_TIMEOUT); + + afterAll(async () => { + if (devbox) { + await devbox.shutdown(); + } + }); + + test('shell.exec - basic execution', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-1'); + const result = await shell.exec('echo "Hello from named shell!"'); + expect(result).toBeDefined(); + expect(result.exitCode).toBe(0); + const output = await result.stdout(); + expect(output).toContain('Hello from named shell!'); + }); + + test('shell.exec - CWD persistence across commands', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-2'); + + // Create a directory and change to it + await shell.exec('mkdir -p /tmp/test-shell-dir'); + await shell.exec('cd /tmp/test-shell-dir'); + + // Verify we're in the new directory + const pwdResult = await shell.exec('pwd'); + const pwd = (await pwdResult.stdout()).trim(); + expect(pwd).toBe('/tmp/test-shell-dir'); + + // Create a file in the current directory + await shell.exec('echo "test content" > testfile.txt'); + + // Verify the file exists in the current directory + const lsResult = await shell.exec('ls testfile.txt'); + expect(lsResult.exitCode).toBe(0); + const lsOutput = await lsResult.stdout(); + expect(lsOutput).toContain('testfile.txt'); + }); + + test('shell.exec - environment variable persistence', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-3'); + + // Set an environment variable + await shell.exec('export TEST_VAR="test-value-123"'); + + // Verify the variable persists in the next command + const echoResult = await shell.exec('echo $TEST_VAR'); + const output = (await echoResult.stdout()).trim(); + expect(output).toBe('test-value-123'); + + // Set another variable and verify both persist + await shell.exec('export ANOTHER_VAR="another-value"'); + const bothResult = await shell.exec('echo "$TEST_VAR:$ANOTHER_VAR"'); + const bothOutput = (await bothResult.stdout()).trim(); + expect(bothOutput).toBe('test-value-123:another-value'); + }); + + test('shell.exec - combined CWD and environment persistence', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-4'); + + // Set environment and change directory + await shell.exec('export PROJECT_DIR="/tmp/my-project"'); + await shell.exec('mkdir -p $PROJECT_DIR'); + await shell.exec('cd $PROJECT_DIR'); + + // Verify both persist + const pwdResult = await shell.exec('pwd'); + const pwd = (await pwdResult.stdout()).trim(); + expect(pwd).toBe('/tmp/my-project'); + + // Create a file using the environment variable + await shell.exec('echo "project file" > $PROJECT_DIR/file.txt'); + + // Verify file exists + const lsResult = await shell.exec('ls file.txt'); + expect(lsResult.exitCode).toBe(0); + }); + + test('shell.execAsync - basic async execution', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-5'); + const execution = await shell.execAsync('sleep 1 && echo "Async command completed"'); + expect(execution).toBeDefined(); + expect(execution.executionId).toBeTruthy(); + + // Wait for completion + const result = await execution.result(); + expect(result.exitCode).toBe(0); + const output = await result.stdout(); + expect(output).toContain('Async command completed'); + }); + + test('shell.execAsync - stateful async execution', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-6'); + + // Set state in first command + await shell.exec('export ASYNC_VAR="async-value"'); + await shell.exec('cd /tmp'); + + // Start async command that uses the state + const execution = await shell.execAsync('echo "CWD: $(pwd), VAR: $ASYNC_VAR"'); + const result = await execution.result(); + + expect(result.exitCode).toBe(0); + const output = await result.stdout(); + expect(output).toContain('CWD: /tmp'); + expect(output).toContain('VAR: async-value'); + }); + + test('shell.exec - sequential execution (queuing)', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-7'); + + // Start multiple commands - they should execute sequentially + const startTime = Date.now(); + await shell.exec('sleep 1 && echo "first"'); + await shell.exec('sleep 1 && echo "second"'); + await shell.exec('sleep 1 && echo "third"'); + const endTime = Date.now(); + + // Verify they took at least 3 seconds (sequential execution) + const duration = endTime - startTime; + expect(duration).toBeGreaterThanOrEqual(2900); // Allow some margin for overhead + + // Verify all commands executed in order + const finalResult = await shell.exec('echo "done"'); + const output = await finalResult.stdout(); + expect(output).toContain('done'); + }); + + test('shell.execAsync - sequential execution with queuing', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-8'); + + // Start multiple async commands - they should queue and execute sequentially + const exec1 = shell.execAsync('sleep 1 && echo "async-first"'); + const exec2 = shell.execAsync('sleep 1 && echo "async-second"'); + const exec3 = shell.execAsync('sleep 1 && echo "async-third"'); + + // Wait for all to complete + const [result1, result2, result3] = await Promise.all([ + (await exec1).result(), + (await exec2).result(), + (await exec3).result(), + ]); + + // Verify all completed successfully + expect(result1.exitCode).toBe(0); + expect(result2.exitCode).toBe(0); + expect(result3.exitCode).toBe(0); + + // Verify outputs + expect(await result1.stdout()).toContain('async-first'); + expect(await result2.stdout()).toContain('async-second'); + expect(await result3.stdout()).toContain('async-third'); + }); + + test('shell.exec - with streaming callbacks', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-9'); + const stdoutLines: string[] = []; + + const result = await shell.exec('echo "line1" && echo "line2" && echo "line3"', { + stdout: (line) => { + stdoutLines.push(line); + }, + }); + + expect(result.success).toBe(true); + expect(result.exitCode).toBe(0); + expect(stdoutLines.length).toBeGreaterThan(0); + const stdoutCombined = stdoutLines.join(''); + expect(stdoutCombined).toContain('line1'); + expect(stdoutCombined).toContain('line2'); + expect(stdoutCombined).toContain('line3'); + // Verify streaming captured same data as result + expect(stdoutCombined).toBe(await result.stdout()); + }); + + test('shell.execAsync - with streaming callbacks', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-10'); + const stdoutLines: string[] = []; + + const execution = await shell.execAsync('echo "async-line1" && sleep 0.5 && echo "async-line2"', { + stdout: (line) => { + stdoutLines.push(line); + }, + }); + + const result = await execution.result(); + expect(result.success).toBe(true); + expect(result.exitCode).toBe(0); + + const stdoutCombined = stdoutLines.join(''); + expect(stdoutCombined).toContain('async-line1'); + expect(stdoutCombined).toContain('async-line2'); + // Verify streaming captured same data as result + expect(stdoutCombined).toBe(await result.stdout()); + }); + + test('multiple named shells - independent state', async () => { + expect(devbox).toBeDefined(); + const shell1 = devbox.shell('independent-shell-1'); + const shell2 = devbox.shell('independent-shell-2'); + + // Set different state in each shell + await shell1.exec('export VAR="shell1-value"'); + await shell1.exec('cd /tmp'); + await shell2.exec('export VAR="shell2-value"'); + await shell2.exec('cd /home'); + + // Verify each shell maintains its own state + const result1 = await shell1.exec('echo "$VAR:$(pwd)"'); + const output1 = (await result1.stdout()).trim(); + expect(output1).toContain('shell1-value'); + expect(output1).toContain('/tmp'); + + const result2 = await shell2.exec('echo "$VAR:$(pwd)"'); + const output2 = (await result2.stdout()).trim(); + expect(output2).toContain('shell2-value'); + expect(output2).toContain('/home'); + }); + }); }); From a1e0cf6bec5261b6bd92e9f9a142e6f0a82505ae Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 25 Nov 2025 16:12:59 -0800 Subject: [PATCH 3/5] cp dines --- src/sdk/devbox.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sdk/devbox.ts b/src/sdk/devbox.ts index 16e80af37..b2e3e36b4 100644 --- a/src/sdk/devbox.ts +++ b/src/sdk/devbox.ts @@ -261,6 +261,8 @@ export class DevboxCmdOps { * Named shell operations for a devbox. * Provides methods for executing commands in a persistent, stateful shell session. * + * Use {@link Devbox.shell} to create a named shell instance. + * * Named shells are stateful and maintain environment variables and the current working directory (CWD) * across commands, just like a real shell on your local computer. Commands executed through the same * named shell instance will execute sequentially - the shell can only run one command at a time with From cf7c317093529dd5299a4c110c79c08b187793f6 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Wed, 26 Nov 2025 13:13:34 -0800 Subject: [PATCH 4/5] cp dines --- src/sdk/devbox.ts | 80 +++++++++-------------------------------------- 1 file changed, 15 insertions(+), 65 deletions(-) diff --git a/src/sdk/devbox.ts b/src/sdk/devbox.ts index b2e3e36b4..09ae2dcb2 100644 --- a/src/sdk/devbox.ts +++ b/src/sdk/devbox.ts @@ -20,6 +20,7 @@ import { PollingOptions } from '../lib/polling'; import { Snapshot } from './snapshot'; import { Execution } from './execution'; import { ExecutionResult } from './execution-result'; +import { uuidv7 } from 'uuidv7'; // Re-export Execution and ExecutionResult for Devbox namespace export { Execution } from './execution'; @@ -261,10 +262,12 @@ export class DevboxCmdOps { * Named shell operations for a devbox. * Provides methods for executing commands in a persistent, stateful shell session. * - * Use {@link Devbox.shell} to create a named shell instance. + * Use {@link Devbox.shell} to create a named shell instance. If you use the same shell name, + * it will re-attach to the existing named shell, preserving its state (environment variables, + * current working directory, etc.). * * Named shells are stateful and maintain environment variables and the current working directory (CWD) - * across commands, just like a real shell on your local computer. Commands executed through the same + * across commands. Commands executed through the same * named shell instance will execute sequentially - the shell can only run one command at a time with * automatic queuing. This ensures that environment changes and directory changes from one command * are preserved for the next command. @@ -286,15 +289,8 @@ export class DevboxNamedShell { * @private */ constructor( - private client: Runloop, - private devboxId: string, + private devbox: Devbox, private shellName: string, - private startStreamingWithCallbacks: ( - executionId: string, - stdout?: (line: string) => void, - stderr?: (line: string) => void, - output?: (line: string) => void, - ) => Promise, ) {} /** @@ -338,40 +334,7 @@ export class DevboxNamedShell { params?: Omit, 'shell_name'> & ExecuteStreamingCallbacks, options?: Core.RequestOptions & { polling?: Partial> }, ): Promise { - const fullParams = { ...params, command, shell_name: this.shellName }; - const hasCallbacks = fullParams.stdout || fullParams.stderr || fullParams.output; - - if (hasCallbacks) { - // With callbacks: use async execution workflow to enable streaming - const { stdout, stderr, output, ...executeParams } = fullParams; - const execution = await this.client.devboxes.executeAsync(this.devboxId, executeParams, options); - - // Start streaming and await both completion and streaming - const streamingPromise = this.startStreamingWithCallbacks( - execution.execution_id, - stdout, - stderr, - output, - ); - - // Wait for both command completion and streaming to finish (using allSettled for robustness) - const results = await Promise.allSettled([ - this.client.devboxes.executions.awaitCompleted(this.devboxId, execution.execution_id, options), - streamingPromise, - ]); - - // Extract command result (throw if it failed, ignore streaming errors) - if (results[0].status === 'rejected') { - throw results[0].reason; - } - const result = results[0].value; - - return new ExecutionResult(this.client, this.devboxId, execution.execution_id, result); - } else { - // Without callbacks: use existing optimized workflow - const result = await this.client.devboxes.executeAndAwaitCompletion(this.devboxId, fullParams, options); - return new ExecutionResult(this.client, this.devboxId, result.execution_id, result); - } + return this.devbox.cmd.exec(command, { ...params, shell_name: this.shellName }, options); } /** @@ -413,18 +376,7 @@ export class DevboxNamedShell { params?: Omit, 'shell_name'> & ExecuteStreamingCallbacks, options?: Core.RequestOptions, ): Promise { - const fullParams = { ...params, command, shell_name: this.shellName }; - const { stdout, stderr, output, ...executeParams } = fullParams; - const execution = await this.client.devboxes.executeAsync(this.devboxId, executeParams, options); - - // Start streaming in background if callbacks provided - let streamingPromise: Promise | undefined; - if (stdout || stderr || output) { - // Start streaming - will be awaited when result() is called - streamingPromise = this.startStreamingWithCallbacks(execution.execution_id, stdout, stderr, output); - } - - return new Execution(this.client, this.devboxId, execution.execution_id, execution, streamingPromise); + return this.devbox.cmd.execAsync(command, { ...params, shell_name: this.shellName }, options); } } @@ -723,9 +675,12 @@ export class Devbox { * * @example * ```typescript - * // Create a named shell + * // Create a named shell with a custom name * const shell = devbox.shell('my-session'); * + * // Create a named shell with an auto-generated UUID name + * const shell2 = devbox.shell(); + * * // Commands execute sequentially and share state * await shell.exec('cd /app'); * await shell.exec('export MY_VAR=value'); @@ -733,16 +688,11 @@ export class Devbox { * await shell.exec('pwd'); // Will output '/app' because CWD is preserved * ``` * - * @param {string} shellName - The name of the persistent shell session + * @param {string} [shellName] - The name of the persistent shell session. If not provided, a UUID will be generated automatically. * @returns {DevboxNamedShell} A {@link DevboxNamedShell} instance for executing commands in the named shell */ - shell(shellName: string): DevboxNamedShell { - return new DevboxNamedShell( - this.client, - this._id, - shellName, - this.startStreamingWithCallbacks.bind(this), - ); + shell(shellName: string = uuidv7()): DevboxNamedShell { + return new DevboxNamedShell(this, shellName); } /** From 55d7ef2fa11229d02d231b317925f4deff73b452 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Wed, 26 Nov 2025 13:15:17 -0800 Subject: [PATCH 5/5] cp dines --- .../smoketests/object-oriented/devbox.test.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/smoketests/object-oriented/devbox.test.ts b/tests/smoketests/object-oriented/devbox.test.ts index 772ffd387..7e6909776 100644 --- a/tests/smoketests/object-oriented/devbox.test.ts +++ b/tests/smoketests/object-oriented/devbox.test.ts @@ -797,5 +797,95 @@ describe('smoketest: object-oriented devbox', () => { expect(output2).toContain('shell2-value'); expect(output2).toContain('/home'); }); + + test('shell.exec - with additional params passed through', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-params'); + + // Test that additional params (like working_dir) are passed through correctly + // Note: shell_name should override any shell_name in params + const result = await shell.exec('pwd', { + working_dir: '/tmp', + }); + + expect(result.exitCode).toBe(0); + const output = (await result.stdout()).trim(); + // Should be in /tmp due to working_dir param + expect(output).toBe('/tmp'); + }); + + test('shell.execAsync - with additional params passed through', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-async-params'); + + // Test that additional params are passed through correctly + const execution = await shell.execAsync('pwd', { + working_dir: '/home', + }); + + const result = await execution.result(); + expect(result.exitCode).toBe(0); + const output = (await result.stdout()).trim(); + // Should be in /home due to working_dir param + expect(output).toBe('/home'); + }); + + test('shell.exec - with stderr streaming callback', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-stderr'); + const stderrLines: string[] = []; + + const result = await shell.exec('echo "error output" >&2', { + stderr: (line) => { + stderrLines.push(line); + }, + }); + + expect(result.success).toBe(true); + expect(result.exitCode).toBe(0); + expect(stderrLines.length).toBeGreaterThan(0); + const stderrCombined = stderrLines.join(''); + expect(stderrCombined).toContain('error output'); + // Verify streaming captured same data as result + expect(stderrCombined).toBe(await result.stderr()); + }); + + test('shell.execAsync - with both stdout and stderr streaming callbacks', async () => { + expect(devbox).toBeDefined(); + const shell = devbox.shell('test-shell-both-streams'); + const stdoutLines: string[] = []; + const stderrLines: string[] = []; + + const execution = await shell.execAsync('echo "to stdout" && echo "to stderr" >&2', { + stdout: (line) => stdoutLines.push(line), + stderr: (line) => stderrLines.push(line), + }); + + const result = await execution.result(); + expect(result.success).toBe(true); + expect(result.exitCode).toBe(0); + + const stdoutCombined = stdoutLines.join(''); + const stderrCombined = stderrLines.join(''); + + expect(stdoutCombined).toContain('to stdout'); + expect(stderrCombined).toContain('to stderr'); + + // Verify streaming captured same data as result + expect(stdoutCombined).toBe(await result.stdout()); + expect(stderrCombined).toBe(await result.stderr()); + }); + + test('shell - auto-generated shell name', async () => { + expect(devbox).toBeDefined(); + // Create shell without providing a name - should auto-generate UUID + const shell = devbox.shell(); + expect(shell).toBeDefined(); + + const result = await shell.exec('echo "test"'); + expect(result.exitCode).toBe(0); + const output = await result.stdout(); + expect(output).toContain('test'); + }); }); });