Skip to content

Commit ad9474f

Browse files
authored
feat(devbox): added devbox.shell(shellName) command and stateful shell class to SDK (#665)
* cp dines * cp dines * cp dines * cp dines * cp dines
1 parent 9edd07f commit ad9474f

2 files changed

Lines changed: 504 additions & 11 deletions

File tree

src/sdk/devbox.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { PollingOptions } from '../lib/polling';
2020
import { Snapshot } from './snapshot';
2121
import { Execution } from './execution';
2222
import { ExecutionResult } from './execution-result';
23+
import { uuidv7 } from 'uuidv7';
2324

2425
// Re-export Execution and ExecutionResult for Devbox namespace
2526
export { Execution } from './execution';
@@ -257,6 +258,128 @@ export class DevboxCmdOps {
257258
}
258259
}
259260

261+
/**
262+
* Named shell operations for a devbox.
263+
* Provides methods for executing commands in a persistent, stateful shell session.
264+
*
265+
* Use {@link Devbox.shell} to create a named shell instance. If you use the same shell name,
266+
* it will re-attach to the existing named shell, preserving its state (environment variables,
267+
* current working directory, etc.).
268+
*
269+
* Named shells are stateful and maintain environment variables and the current working directory (CWD)
270+
* across commands. Commands executed through the same
271+
* named shell instance will execute sequentially - the shell can only run one command at a time with
272+
* automatic queuing. This ensures that environment changes and directory changes from one command
273+
* are preserved for the next command.
274+
*
275+
* @example
276+
* ```typescript
277+
* // Create a named shell
278+
* const shell = devbox.shell('my-session');
279+
*
280+
* // Commands execute sequentially and share state
281+
* await shell.exec('cd /app');
282+
* await shell.exec('export MY_VAR=value');
283+
* await shell.exec('echo $MY_VAR'); // Will output 'value' because env is preserved
284+
* await shell.exec('pwd'); // Will output '/app' because CWD is preserved
285+
* ```
286+
*/
287+
export class DevboxNamedShell {
288+
/**
289+
* @private
290+
*/
291+
constructor(
292+
private devbox: Devbox,
293+
private shellName: string,
294+
) {}
295+
296+
/**
297+
* Execute a command in the named shell and wait for it to complete.
298+
* Optionally provide callbacks to stream logs in real-time.
299+
*
300+
* The command will execute in the persistent shell session, maintaining environment variables
301+
* and the current working directory from previous commands. Commands are queued and execute
302+
* sequentially - only one command runs at a time in the named shell.
303+
*
304+
* When callbacks are provided, this method waits for both the command to complete
305+
* AND all streaming data to be processed before returning.
306+
*
307+
* @example
308+
* ```typescript
309+
* const shell = devbox.shell('my-session');
310+
*
311+
* // Simple execution
312+
* const result = await shell.exec('ls -la');
313+
* console.log(await result.stdout());
314+
*
315+
* // With streaming callbacks
316+
* const result = await shell.exec('npm install', {
317+
* stdout: (line) => process.stdout.write(line),
318+
* stderr: (line) => process.stderr.write(line),
319+
* });
320+
*
321+
* // Stateful execution - environment and CWD are preserved
322+
* await shell.exec('cd /app');
323+
* await shell.exec('export NODE_ENV=production');
324+
* const result = await shell.exec('npm start'); // Runs in /app with NODE_ENV=production
325+
* ```
326+
*
327+
* @param {string} command - The command to execute
328+
* @param {Omit<Omit<DevboxExecuteParams, 'command'>, 'shell_name'> & ExecuteStreamingCallbacks} [params] - Optional parameters (shell_name is automatically set)
329+
* @param {Core.RequestOptions & { polling?: Partial<PollingOptions<DevboxAsyncExecutionDetailView>> }} [options] - Request options with optional polling configuration
330+
* @returns {Promise<ExecutionResult>} {@link ExecutionResult} with stdout, stderr, and exit status
331+
*/
332+
async exec(
333+
command: string,
334+
params?: Omit<Omit<DevboxExecuteParams, 'command'>, 'shell_name'> & ExecuteStreamingCallbacks,
335+
options?: Core.RequestOptions & { polling?: Partial<PollingOptions<DevboxAsyncExecutionDetailView>> },
336+
): Promise<ExecutionResult> {
337+
return this.devbox.cmd.exec(command, { ...params, shell_name: this.shellName }, options);
338+
}
339+
340+
/**
341+
* Execute a command in the named shell asynchronously without waiting for completion.
342+
* Optionally provide callbacks to stream logs in real-time as they are produced.
343+
*
344+
* The command will execute in the persistent shell session, maintaining environment variables
345+
* and the current working directory from previous commands. Commands are queued and execute
346+
* sequentially - only one command runs at a time in the named shell.
347+
*
348+
* Callbacks fire in real-time as logs arrive. When you call execution.result(),
349+
* it will wait for both the command to complete and all streaming to finish.
350+
*
351+
* @example
352+
* ```typescript
353+
* const shell = devbox.shell('my-session');
354+
*
355+
* const execution = await shell.execAsync('long-running-task.sh', {
356+
* stdout: (line) => console.log(`[LOG] ${line}`),
357+
* });
358+
*
359+
* // Do other work while command runs...
360+
* // Note: if you call shell.exec() or shell.execAsync() again, it will queue
361+
* // and wait for this command to complete first
362+
*
363+
* const result = await execution.result();
364+
* if (result.success) {
365+
* console.log('Task completed successfully!');
366+
* }
367+
* ```
368+
*
369+
* @param {string} command - The command to execute
370+
* @param {Omit<Omit<DevboxExecuteAsyncParams, 'command'>, 'shell_name'> & ExecuteStreamingCallbacks} [params] - Optional parameters (shell_name is automatically set)
371+
* @param {Core.RequestOptions} [options] - Request options
372+
* @returns {Promise<Execution>} {@link Execution} object for tracking and controlling the command
373+
*/
374+
async execAsync(
375+
command: string,
376+
params?: Omit<Omit<DevboxExecuteAsyncParams, 'command'>, 'shell_name'> & ExecuteStreamingCallbacks,
377+
options?: Core.RequestOptions,
378+
): Promise<Execution> {
379+
return this.devbox.cmd.execAsync(command, { ...params, shell_name: this.shellName }, options);
380+
}
381+
}
382+
260383
/**
261384
* File operations for a devbox.
262385
* Provides methods for reading, writing, uploading, and downloading files.
@@ -541,6 +664,37 @@ export class Devbox {
541664
return this._id;
542665
}
543666

667+
/**
668+
* Create a named shell instance for stateful command execution.
669+
*
670+
* Named shells are stateful and maintain environment variables and the current working directory (CWD)
671+
* across commands, just like a real shell on your local computer. Commands executed through the same
672+
* named shell instance will execute sequentially - the shell can only run one command at a time with
673+
* automatic queuing. This ensures that environment changes and directory changes from one command
674+
* are preserved for the next command.
675+
*
676+
* @example
677+
* ```typescript
678+
* // Create a named shell with a custom name
679+
* const shell = devbox.shell('my-session');
680+
*
681+
* // Create a named shell with an auto-generated UUID name
682+
* const shell2 = devbox.shell();
683+
*
684+
* // Commands execute sequentially and share state
685+
* await shell.exec('cd /app');
686+
* await shell.exec('export MY_VAR=value');
687+
* await shell.exec('echo $MY_VAR'); // Will output 'value' because env is preserved
688+
* await shell.exec('pwd'); // Will output '/app' because CWD is preserved
689+
* ```
690+
*
691+
* @param {string} [shellName] - The name of the persistent shell session. If not provided, a UUID will be generated automatically.
692+
* @returns {DevboxNamedShell} A {@link DevboxNamedShell} instance for executing commands in the named shell
693+
*/
694+
shell(shellName: string = uuidv7()): DevboxNamedShell {
695+
return new DevboxNamedShell(this, shellName);
696+
}
697+
544698
/**
545699
* Start streaming logs with callbacks.
546700
* Returns a promise that resolves when all streams complete.
@@ -799,5 +953,10 @@ export declare namespace Devbox {
799953
* @see {@link ExecuteStreamingCallbacks}
800954
*/
801955
type ExecuteStreamingCallbacks as ExecuteStreamingCallbacks,
956+
/**
957+
* Named shell operations class for stateful command execution.
958+
* @see {@link DevboxNamedShell}
959+
*/
960+
DevboxNamedShell as NamedShell,
802961
};
803962
}

0 commit comments

Comments
 (0)