@@ -278,6 +278,39 @@ def net(self) -> AsyncNetworkInterface:
278278 """
279279 return AsyncNetworkInterface (self )
280280
281+ def shell (self , shell_name : str | None = None ) -> AsyncNamedShell :
282+ """Create a named shell instance for stateful command execution.
283+
284+ Named shells are stateful and maintain environment variables and the current working
285+ directory (CWD) across commands, just like a real shell on your local computer.
286+ Commands executed through the same named shell instance will execute sequentially -
287+ the shell can only run one command at a time with automatic queuing. This ensures
288+ that environment changes and directory changes from one command are preserved for
289+ the next command.
290+
291+ :param shell_name: The name of the persistent shell session. If not provided, a UUID will be generated automatically.
292+ :type shell_name: str | None, optional
293+ :return: An AsyncNamedShell instance for executing commands in the named shell
294+ :rtype: AsyncNamedShell
295+
296+ Example:
297+ >>> # Create a named shell with a custom name
298+ >>> shell = await devbox.shell("my-session")
299+ >>> # Create a named shell with an auto-generated UUID name
300+ >>> shell2 = await devbox.shell()
301+ >>> # Commands execute sequentially and share state
302+ >>> await shell.exec("cd /app")
303+ >>> await shell.exec("export MY_VAR=value")
304+ >>> result = await shell.exec("echo $MY_VAR") # Will output 'value'
305+ >>> result = await shell.exec("pwd") # Will output '/app'
306+ """
307+ if shell_name is None :
308+ # uuid_utils is not typed
309+ from uuid_utils import uuid7 # type: ignore
310+
311+ shell_name = str (uuid7 ())
312+ return AsyncNamedShell (self , shell_name )
313+
281314 # ------------------------------------------------------------------ #
282315 # Internal helpers
283316 # ------------------------------------------------------------------ #
@@ -550,6 +583,105 @@ async def upload(
550583 )
551584
552585
586+ class AsyncNamedShell :
587+ """Interface for executing commands in a persistent, stateful shell session.
588+
589+ Named shells are stateful and maintain environment variables and the current working
590+ directory (CWD) across commands. Commands executed through the same named shell
591+ instance will execute sequentially - the shell can only run one command at a time
592+ with automatic queuing. This ensures that environment changes and directory changes
593+ from one command are preserved for the next command.
594+
595+ Use :meth:`AsyncDevbox.shell` to create a named shell instance. If you use the same
596+ shell name, it will re-attach to the existing named shell, preserving its state.
597+
598+ Example:
599+ >>> shell = await devbox.shell("my-session")
600+ >>> await shell.exec("cd /app")
601+ >>> await shell.exec("export MY_VAR=value")
602+ >>> result = await shell.exec("echo $MY_VAR") # Will output 'value'
603+ >>> result = await shell.exec("pwd") # Will output '/app'
604+ """
605+
606+ def __init__ (self , devbox : AsyncDevbox , shell_name : str ) -> None :
607+ """Initialize the named shell.
608+
609+ :param devbox: The async devbox instance to execute commands on
610+ :type devbox: AsyncDevbox
611+ :param shell_name: The name of the persistent shell session
612+ :type shell_name: str
613+ """
614+ self ._devbox = devbox
615+ self ._shell_name = shell_name
616+
617+ async def exec (
618+ self ,
619+ command : str ,
620+ ** params : Unpack [SDKDevboxExecuteParams ],
621+ ) -> AsyncExecutionResult :
622+ """Execute a command in the named shell and wait for it to complete.
623+
624+ The command will execute in the persistent shell session, maintaining environment
625+ variables and the current working directory from previous commands. Commands are
626+ queued and execute sequentially - only one command runs at a time in the named shell.
627+
628+ Optionally provide callbacks to stream logs in real-time. When callbacks are provided,
629+ this method waits for both the command to complete AND all streaming data to be
630+ processed before returning.
631+
632+ :param command: The command to execute
633+ :type command: str
634+ :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDevboxExecuteParams` for available parameters
635+ :return: Wrapper with exit status and output helpers
636+ :rtype: AsyncExecutionResult
637+
638+ Example:
639+ >>> shell = await devbox.shell("my-session")
640+ >>> result = await shell.exec("ls -la")
641+ >>> print(await result.stdout())
642+ >>> # With streaming callbacks
643+ >>> result = await shell.exec("npm install", stdout=lambda line: print(f"[LOG] {line}"))
644+ """
645+ # Ensure shell_name is set and cannot be overridden by user params
646+ params = dict (params )
647+ params ["shell_name" ] = self ._shell_name
648+ return await self ._devbox .cmd .exec (command , ** params )
649+
650+ async def exec_async (
651+ self ,
652+ command : str ,
653+ ** params : Unpack [SDKDevboxExecuteAsyncParams ],
654+ ) -> AsyncExecution :
655+ """Execute a command in the named shell asynchronously without waiting for completion.
656+
657+ The command will execute in the persistent shell session, maintaining environment
658+ variables and the current working directory from previous commands. Commands are
659+ queued and execute sequentially - only one command runs at a time in the named shell.
660+
661+ Optionally provide callbacks to stream logs in real-time as they are produced.
662+ Callbacks fire in real-time as logs arrive. When you call execution.result(),
663+ it will wait for both the command to complete and all streaming to finish.
664+
665+ :param command: The command to execute
666+ :type command: str
667+ :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDevboxExecuteAsyncParams` for available parameters
668+ :return: Handle for managing the running process
669+ :rtype: AsyncExecution
670+
671+ Example:
672+ >>> shell = await devbox.shell("my-session")
673+ >>> execution = await shell.exec_async("long-running-task.sh", stdout=lambda line: print(f"[LOG] {line}"))
674+ >>> # Do other work while command runs...
675+ >>> result = await execution.result()
676+ >>> if result.success:
677+ ... print("Task completed successfully!")
678+ """
679+ # Ensure shell_name is set and cannot be overridden by user params
680+ params = dict (params )
681+ params ["shell_name" ] = self ._shell_name
682+ return await self ._devbox .cmd .exec_async (command , ** params )
683+
684+
553685class AsyncNetworkInterface :
554686 """Interface for networking operations on a devbox.
555687
0 commit comments