-
Notifications
You must be signed in to change notification settings - Fork 1
feat(devbox): added devbox.shell(shellName) command and stateful shell class to SDK #696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -280,6 +280,39 @@ def net(self) -> NetworkInterface: | |
| """ | ||
| return NetworkInterface(self) | ||
|
|
||
| def shell(self, shell_name: str | None = None) -> NamedShell: | ||
| """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. | ||
|
|
||
| :param shell_name: The name of the persistent shell session. If not provided, a UUID will be generated automatically. | ||
| :type shell_name: str | None, optional | ||
| :return: A NamedShell instance for executing commands in the named shell | ||
| :rtype: NamedShell | ||
|
|
||
| Example: | ||
| >>> # Create a named shell with a custom name | ||
| >>> shell = devbox.shell("my-session") | ||
| >>> # Create a named shell with an auto-generated UUID name | ||
| >>> shell2 = devbox.shell() | ||
| >>> # Commands execute sequentially and share state | ||
| >>> shell.exec("cd /app") | ||
| >>> shell.exec("export MY_VAR=value") | ||
| >>> result = shell.exec("echo $MY_VAR") # Will output 'value' | ||
| >>> result = shell.exec("pwd") # Will output '/app' | ||
| """ | ||
| if shell_name is None: | ||
| # uuid_utils is not typed | ||
| from uuid_utils import uuid7 # type: ignore | ||
|
|
||
| shell_name = str(uuid7()) | ||
| return NamedShell(self, shell_name) | ||
|
|
||
| # --------------------------------------------------------------------- # | ||
| # Internal helpers | ||
| # --------------------------------------------------------------------- # | ||
|
|
@@ -558,6 +591,105 @@ def upload( | |
| ) | ||
|
|
||
|
|
||
| class NamedShell: | ||
| """Interface 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. 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. | ||
|
|
||
| Use :meth:`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. | ||
|
|
||
| Example: | ||
| >>> shell = devbox.shell("my-session") | ||
| >>> shell.exec("cd /app") | ||
| >>> shell.exec("export MY_VAR=value") | ||
| >>> result = shell.exec("echo $MY_VAR") # Will output 'value' | ||
| >>> result = shell.exec("pwd") # Will output '/app' | ||
| """ | ||
|
|
||
| def __init__(self, devbox: Devbox, shell_name: str) -> None: | ||
| """Initialize the named shell. | ||
|
|
||
| :param devbox: The devbox instance to execute commands on | ||
| :type devbox: Devbox | ||
| :param shell_name: The name of the persistent shell session | ||
| :type shell_name: str | ||
| """ | ||
| self._devbox = devbox | ||
| self._shell_name = shell_name | ||
|
|
||
| def exec( | ||
| self, | ||
| command: str, | ||
| **params: Unpack[SDKDevboxExecuteParams], | ||
| ) -> ExecutionResult: | ||
| """Execute a command in the named shell and wait for it to complete. | ||
|
|
||
| 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. | ||
|
|
||
| Optionally provide callbacks to stream logs in real-time. When callbacks are provided, | ||
| this method waits for both the command to complete AND all streaming data to be | ||
| processed before returning. | ||
|
|
||
| :param command: The command to execute | ||
| :type command: str | ||
| :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDevboxExecuteParams` for available parameters | ||
| :return: Wrapper with exit status and output helpers | ||
| :rtype: ExecutionResult | ||
|
|
||
| Example: | ||
| >>> shell = devbox.shell("my-session") | ||
| >>> result = shell.exec("ls -la") | ||
| >>> print(result.stdout()) | ||
| >>> # With streaming callbacks | ||
| >>> result = shell.exec("npm install", stdout=lambda line: print(f"[LOG] {line}")) | ||
| """ | ||
| # Ensure shell_name is set and cannot be overridden by user params | ||
| params = dict(params) | ||
| params["shell_name"] = self._shell_name | ||
| return self._devbox.cmd.exec(command, **params) | ||
|
|
||
| def exec_async( | ||
| self, | ||
| command: str, | ||
| **params: Unpack[SDKDevboxExecuteAsyncParams], | ||
| ) -> Execution: | ||
| """Execute a command in the named shell asynchronously without waiting for completion. | ||
|
|
||
| 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. | ||
|
|
||
| Optionally provide callbacks to stream logs in real-time as they are produced. | ||
| 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. | ||
|
|
||
| :param command: The command to execute | ||
| :type command: str | ||
| :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKDevboxExecuteAsyncParams` for available parameters | ||
| :return: Handle for managing the running process | ||
| :rtype: Execution | ||
|
|
||
| Example: | ||
| >>> shell = devbox.shell("my-session") | ||
| >>> execution = shell.exec_async("long-running-task.sh", stdout=lambda line: print(f"[LOG] {line}")) | ||
| >>> # Do other work while command runs... | ||
| >>> result = execution.result() | ||
| >>> if result.success: | ||
| ... print("Task completed successfully!") | ||
| """ | ||
| # Ensure shell_name is set and cannot be overridden by user params | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it worth trying to make sure that the shell_name parameter is not exposed to the user at all? would probably be a hassle with types
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we want them to name their shells but we don't need to require it. |
||
| params = dict(params) | ||
| params["shell_name"] = self._shell_name | ||
| return self._devbox.cmd.exec_async(command, **params) | ||
|
|
||
|
|
||
| class NetworkInterface: | ||
| """Interface for network operations on a devbox. | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we provide a way to close the stateful shell? how do we handle how long a stateful shell remains alive on the backend?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also since we're providing this as a first class object, it might make sense to provide a per-shell command history as a convenience method. what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good, future improvements