@@ -20,6 +20,7 @@ import { PollingOptions } from '../lib/polling';
2020import { Snapshot } from './snapshot' ;
2121import { Execution } from './execution' ;
2222import { ExecutionResult } from './execution-result' ;
23+ import { uuidv7 } from 'uuidv7' ;
2324
2425// Re-export Execution and ExecutionResult for Devbox namespace
2526export { 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