feat(exec): add mino exec subcommand for running commands in sessions#42
feat(exec): add mino exec subcommand for running commands in sessions#42
Conversation
Implement a new `mino exec` subcommand that executes commands inside running session containers with exit code propagation. Includes: - ExecArgs CLI struct with optional session name and `-- command` separator - Session resolution logic: named session or most recent running session - exec_in_container method on ContainerRuntime trait with 3 implementations (NativePodmanRuntime, OrbStackRuntime, MockRuntime) - Exit code propagation through main() via MinoResult<ExitCode> - TTY detection for interactive shell usage - NoActiveSessions error with helpful hint Tests: 18 new unit tests covering CLI parsing, pure resolution logic, and MockRuntime integration. All tests validate behavior rather than implementation. Co-Authored-By: Claude <noreply@anthropic.com>
src/cli/commands/exec.rs
Outdated
|
|
||
| const DEFAULT_SHELL: &str = "/bin/zsh"; | ||
|
|
||
| pub async fn execute(args: ExecArgs, config: &Config) -> MinoResult<ExitCode> { |
There was a problem hiding this comment.
Return Type Inconsistency
This function returns MinoResult<ExitCode> while all 9 other command modules return MinoResult<()>. This breaks the uniform command interface established across the codebase.
Flagged by: Architecture (HIGH), Consistency (HIGH), Complexity (HIGH)
Recommendation: Change exec::execute() to return MinoResult<()> and call std::process::exit() internally for non-zero codes (same pattern run could adopt):
pub async fn execute(args: ExecArgs, config: &Config) -> MinoResult<()> {
// ... existing logic ...
let exit_code = exec_in_session(&session, &*runtime, &command, tty).await?;
if exit_code != 0 {
std::process::exit(exit_code as i32);
}
Ok(())
}This aligns with the existing pattern where other commands (like run) handle exit code semantics internally and return Ok(()) to main.rs.
Claude Code Review
| let session = manager | ||
| .get(name) | ||
| .await? | ||
| .ok_or_else(|| MinoError::SessionNotFound(name.to_string()))?; |
There was a problem hiding this comment.
S-1: Session Name Validation Missing
The session argument from ExecArgs (user-supplied CLI input) is passed directly to SessionManager::get(), which constructs a file path without validation. A user could supply path traversal strings like ../../../etc/passwd.
Flagged by: Security (MEDIUM)
This is a defense-in-depth violation. The codebase already has validate_layer_name() in src/layer/resolve.rs that prevents path traversal.
Recommendation: Add session name validation:
fn validate_session_name(name: &str) -> MinoResult<()> {
if name.is_empty()
|| name.contains('/')
|| name.contains('\\')
|| name.contains("..")
|| name.contains('\0')
|| \!name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err(MinoError::User(format\!(
"Invalid session name '{}': must contain only alphanumeric characters, hyphens, or underscores",
name
)));
}
Ok(())
}Call this in resolve_session() or at the Session::load() boundary.
Claude Code Review
src/cli/commands/exec.rs
Outdated
| return Err(MinoError::User(format!( | ||
| "Session '{}' is not running (status: {}). Use 'mino list' to see active sessions.", | ||
| session.name, | ||
| format!("{:?}", session.status).to_lowercase() |
There was a problem hiding this comment.
B-1: Debug Formatting for User-Facing Status String
The validate_session_running function formats SessionStatus using {:?} (Debug) and lowercases it:
format!("{:?}", session.status).to_lowercase()This couples user-facing error messages to Rust's Debug trait output. If SessionStatus adds multi-word variants (e.g., ShuttingDown), the output becomes "shuttingdown" which is not readable.
Flagged by: Rust (HIGH), Complexity (MEDIUM)
Recommendation: Implement Display for SessionStatus in src/session/state.rs:
impl std::fmt::Display for SessionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Starting => write!(f, "starting"),
Self::Running => write!(f, "running"),
Self::Stopped => write!(f, "stopped"),
Self::Failed => write!(f, "failed"),
}
}
}Then use {} formatting in exec.rs instead of {:?}.to_lowercase().
Claude Code Review
src/cli/commands/exec.rs
Outdated
| let tty = std::io::stdin().is_terminal(); | ||
| let exit_code = exec_in_session(&session, &*runtime, &command, tty).await?; | ||
|
|
||
| Ok(ExitCode::from((exit_code & 0xFF) as u8)) |
There was a problem hiding this comment.
B-2: Exit Code Truncation Lacks Logging
The exit code is masked and cast to u8:
Ok(ExitCode::from((exit_code & 0xFF) as u8))When exec_interactive returns -1 (process killed by signal, no exit code), this silently converts to 255. While POSIX-correct, the distinction between "exited with 255" and "killed by signal" is lost.
Flagged by: Regression (HIGH), Security (MEDIUM)
Recommendation: Add explicit handling and logging:
let exit_code = exec_in_session(&session, &*runtime, &command, tty).await?;
if exit_code < 0 {
debug!("Container process terminated by signal (raw exit code: {})", exit_code);
}
Ok(ExitCode::from((exit_code & 0xFF) as u8))This documents the behavior and aids debugging when a process is killed by signal.
Claude Code Review
src/main.rs
Outdated
| Commands::Completions(_) => unreachable!("Completions handled above"), | ||
| Commands::Init(_) | Commands::Completions(_) => unreachable!("handled above"), | ||
| Commands::Exec(args) => mino::cli::commands::exec(args, &config).await, | ||
| Commands::Run(args) => { mino::cli::commands::run(args, &config).await?; Ok(ExitCode::SUCCESS) } |
There was a problem hiding this comment.
SF-3: Dispatch Boilerplate - Inconsistent Match Arm Formatting
The dispatch match now has two distinct patterns in a single block:
Commands::Exec(args) => mino::cli::commands::exec(args, &config).await,
Commands::Run(args) => { mino::cli::commands::run(args, &config).await?; Ok(ExitCode::SUCCESS) }The single-line { ...; Ok(ExitCode::SUCCESS) } pattern repeated 8 times is boilerplate that obscures the dispatch table.
Flagged by: Complexity (HIGH), Consistency (MEDIUM), Rust (HIGH)
Recommendation: Extract a helper to eliminate duplication:
async fn ok_success<F, Fut>(f: F) -> MinoResult<ExitCode>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = MinoResult<()>>,
{
f().await?;
Ok(ExitCode::SUCCESS)
}
// Then:
Commands::Run(args) => ok_success(mino::cli::commands::run(args, &config)).await,
Commands::Exec(args) => mino::cli::commands::exec(args, &config).await,Alternatively, align with the note under issue SF-1 and have all commands return MinoResult<()>, handling exit codes internally (preferred long-term).
Claude Code Review
| use std::process::ExitCode; | ||
|
|
||
| const DEFAULT_SHELL: &str = "/bin/zsh"; | ||
|
|
There was a problem hiding this comment.
SF-2: Missing Doc Comment on execute Function
The execute() function lacks a doc comment. Every other command module has:
/// Execute the {name} command
pub async fn execute(...) -> MinoResult<()>Flagged by: Consistency (MEDIUM)
Recommendation: Add the doc comment:
/// Execute the exec command
pub async fn execute(args: ExecArgs, config: &Config) -> MinoResult<ExitCode> {This maintains consistency with established conventions across all command modules.
Claude Code Review
PR #42 Code Review SummaryReview Scores by Discipline
Average Score: 7.6/10 Consolidated Issues SummaryBLOCKING / HIGH SEVERITY (5 issues)
SHOULD FIX (5 issues)
INFORMATIONAL / PRE-EXISTING (6 issues)
Positive Observations
Merge RecommendationCHANGES_REQUESTED - The PR has architectural merit and the exec command itself is well-designed. However, it introduces a pattern deviation (return type inconsistency) that will compound maintenance burden as more commands need exit code propagation. Conditions for Merge
Inline CommentsSee 6 inline comments on the PR for specific line-level feedback. Generated by Claude Code Review |
…in_container Build Vec<&str> directly instead of Vec<String> followed by conversion. All references (static literals, container_id: &str, command: &[String]) have sufficient lifetimes. Matches the pattern used by NativePodmanRuntime and other OrbStack methods (start_attached, stop, kill, etc.). Co-Authored-By: Claude <noreply@anthropic.com>
- Implement Display trait for SessionStatus with explicit match arms, replacing fragile Debug-based formatting in exec.rs and stop.rs - Add validate_session_name() to reject path traversal, null bytes, and special characters in user-supplied session names. Applied in both Session::load() and Session::create_file() for defense in depth - Export validate_session_name from session module for reuse - Add comprehensive tests for both Display and name validation Addresses review issues I-2, I-3. Co-Authored-By: Claude <noreply@anthropic.com>
- Return MinoResult<()> instead of MinoResult<ExitCode>, using std::process::exit() for non-zero exit codes to preserve exit code semantics while keeping dispatch uniform with all other commands - Simplify main.rs dispatch: all arms now use `?` with a single Ok(ExitCode::SUCCESS) return, eliminating per-arm wrapping - Replace raw eprintln!() with ui::step_info() matching stop.rs pattern - Add tracing::debug! for exit code before truncation so signal-killed processes (-1 -> 255) are visible in debug logs - Add /// doc comment matching other command modules - Add validate_running_rejects_failed test for complete coverage - Remove redundant exec_tty_false_forwarded test (duplicates exec_delegates_to_runtime which already covers tty:false) Addresses review issues I-1, I-5, I-6, I-7, I-8, I-9. Co-Authored-By: Claude <noreply@anthropic.com>
Summary
This implements
mino exec— a new subcommand that executes a command inside a running mino session container with full exit code propagation.Changes
Core Feature:
ExecArgsCLI struct with optional session name and-- commandseparator/bin/zsh)main.rs(MinoResult<ExitCode>)Trait Extension:
exec_in_container()method toContainerRuntimetraitNativePodmanRuntime,OrbStackRuntime,MockRuntimepodman exec -i(+-tif TTY) with interactive stream handlingError Handling:
NoActiveSessionserror with actionable hint: "Start a session with: mino run"CLI Argument Parsing:
mino exec— defaults to most recent running, shell /bin/zshmino exec SESSION_NAME— exec into named sessionmino exec -- ls -la— run command in most recent sessionmino exec SESSION -- ls -la— run command in named sessionTesting
18 new unit tests covering:
All tests validate behavior rather than implementation details.
Breaking Changes
None. Exit code propagation in
main()is transparent to existing commands (all returnOk(ExitCode::SUCCESS)).Related Issues
Closes #42