Files
codex/codex-rs/core/src/shell.rs
T
pakrym-oai 6a6a5f925e [codex] Add environment shell info (#26480)
## Why

Shell detection needs to be available through the `Environment`
abstraction so callers can ask the selected local or remote environment
for shell metadata without adding a separate HTTP endpoint or parallel
info-source path. This keeps shell metadata shaped like the existing
environment-owned filesystem capability and lets remote environments
answer through exec-server JSON-RPC.

## What changed

- Added `environment/info` to the exec-server protocol/client/server and
exposed `Environment::info()`.
- Added local and remote environment info providers on `Environment`,
following the existing capability-provider pattern used for filesystem
access.
- Moved the shared shell detection logic into `codex-shell-command` and
kept core shell APIs as wrappers around that implementation.
- Returned shell metadata as `EnvironmentInfo { shell: ShellInfo }`
using the existing shell detection path.
- Added a remote environment test that calls `Environment::info()`
through an exec-server-backed environment.

## Validation

- `git diff --check`
- `just test -p codex-shell-command`
- `just test -p codex-core -E 'test(/shell::tests::/)'`\n- `just test -p
codex-exec-server environment`
2026-06-04 22:36:25 -07:00

114 lines
3.5 KiB
Rust

use crate::shell_snapshot::ShellSnapshot;
use codex_shell_command::shell_detect::DetectedShell;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::watch;
pub use codex_shell_command::shell_detect::ShellType;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Shell {
pub(crate) shell_type: ShellType,
pub(crate) shell_path: PathBuf,
#[serde(
skip_serializing,
skip_deserializing,
default = "empty_shell_snapshot_receiver"
)]
pub(crate) shell_snapshot: watch::Receiver<Option<Arc<ShellSnapshot>>>,
}
impl Shell {
pub fn name(&self) -> &'static str {
self.shell_type.name()
}
/// Takes a string of shell and returns the full list of command args to
/// use with `exec()` to run the shell command.
pub fn derive_exec_args(&self, command: &str, use_login_shell: bool) -> Vec<String> {
match self.shell_type {
ShellType::Zsh | ShellType::Bash | ShellType::Sh => {
let arg = if use_login_shell { "-lc" } else { "-c" };
vec![
self.shell_path.to_string_lossy().to_string(),
arg.to_string(),
command.to_string(),
]
}
ShellType::PowerShell => {
let mut args = vec![self.shell_path.to_string_lossy().to_string()];
if !use_login_shell {
args.push("-NoProfile".to_string());
}
args.push("-Command".to_string());
args.push(command.to_string());
args
}
ShellType::Cmd => {
let mut args = vec![self.shell_path.to_string_lossy().to_string()];
args.push("/c".to_string());
args.push(command.to_string());
args
}
}
}
/// Return the shell snapshot if existing.
pub fn shell_snapshot(&self) -> Option<Arc<ShellSnapshot>> {
self.shell_snapshot.borrow().clone()
}
}
pub(crate) fn empty_shell_snapshot_receiver() -> watch::Receiver<Option<Arc<ShellSnapshot>>> {
let (_tx, rx) = watch::channel(None);
rx
}
impl PartialEq for Shell {
fn eq(&self, other: &Self) -> bool {
self.shell_type == other.shell_type && self.shell_path == other.shell_path
}
}
impl Eq for Shell {}
impl From<DetectedShell> for Shell {
fn from(detected: DetectedShell) -> Self {
Self {
shell_type: detected.shell_type,
shell_path: detected.shell_path,
shell_snapshot: empty_shell_snapshot_receiver(),
}
}
}
#[cfg(all(test, unix))]
fn ultimate_fallback_shell() -> Shell {
codex_shell_command::shell_detect::ultimate_fallback_shell().into()
}
pub fn get_shell_by_model_provided_path(shell_path: &PathBuf) -> Shell {
codex_shell_command::shell_detect::get_shell_by_model_provided_path(shell_path).into()
}
pub fn get_shell(shell_type: ShellType, path: Option<&PathBuf>) -> Option<Shell> {
codex_shell_command::shell_detect::get_shell(shell_type, path).map(Into::into)
}
pub fn default_user_shell() -> Shell {
codex_shell_command::shell_detect::default_user_shell().into()
}
#[cfg(all(test, target_os = "macos"))]
fn default_user_shell_from_path(user_shell_path: Option<PathBuf>) -> Shell {
codex_shell_command::shell_detect::default_user_shell_from_path(user_shell_path).into()
}
#[cfg(test)]
#[cfg(unix)]
#[path = "shell_tests.rs"]
mod tests;