Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/runbox-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ log = "0.4"
[features]
# Enable tests that require tmux to be installed
tmux-tests = []
# Enable tests that require zellij to be installed
zellij-tests = []

[dev-dependencies]
assert_cmd = "2"
Expand Down
19 changes: 10 additions & 9 deletions crates/runbox-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ RELATED COMMANDS:
/// Variable bindings (key=value) - only for template mode
#[arg(short, long)]
binding: Vec<String>,
/// Runtime environment (bg, background, tmux)
/// Runtime environment (bg, background, tmux, zellij)
#[arg(long, default_value = "bg")]
runtime: RuntimeType,
/// Skip execution (dry run)
Expand Down Expand Up @@ -163,7 +163,7 @@ RELATED COMMANDS:
runbox ps List runs to check status
runbox logs View stdout/stderr from a run")]
Log {
/// Runtime environment (bg, background, tmux)
/// Runtime environment (bg, background, tmux, zellij)
#[arg(long, default_value = "bg")]
runtime: RuntimeType,
/// Skip execution (dry run)
Expand Down Expand Up @@ -364,7 +364,7 @@ NOTES:
RELATED COMMANDS:
runbox ps List runs to find run IDs
runbox show Show run metadata including log file path
runbox attach Attach to tmux session (for tmux runtime)")]
runbox attach Attach to tmux/zellij session (interactive runtime)")]
Logs {
/// Run ID (or short ID prefix, e.g., '550e8400')
run_id: String,
Expand All @@ -375,21 +375,22 @@ RELATED COMMANDS:
#[arg(short, long)]
lines: Option<usize>,
},
/// Attach to a running tmux session for interactive access
/// Attach to a running tmux/zellij session for interactive access
#[command(after_help = "\
EXAMPLES:
# Attach to a tmux-based run
# Attach to a tmux/zellij-based run
runbox attach 550e8400
# Using full run ID
runbox attach run_550e8400-e29b-41d4-a716-446655440000
NOTES:
- Only works for runs started with --runtime tmux or --runtime zellij
- Use Ctrl+B, D to detach from the tmux session
- Detach from tmux with Ctrl+B, D or from zellij with Ctrl+O, D
- The process continues running after detaching
RELATED COMMANDS:
runbox ps List runs to find run IDs
runbox logs <id> View logs (for background runs)
runbox run --runtime tmux Start a new run in tmux")]
runbox run --runtime tmux Start a new run in tmux
runbox run --runtime zellij Start a new run in zellij")]
Attach {
/// Run ID (or short ID prefix, e.g., '550e8400')
run_id: String,
Expand Down Expand Up @@ -926,7 +927,7 @@ NOTES:
/// Variable bindings (key=value) for template
#[arg(short, long)]
binding: Vec<String>,
/// Runtime environment (bg, background, tmux)
/// Runtime environment (bg, background, tmux, zellij)
#[arg(short, long, default_value = "bg")]
runtime: RuntimeType,
/// Show what would be executed without running
Expand Down Expand Up @@ -1713,7 +1714,7 @@ fn cmd_run_replay(
println!("Short ID: {}", run.short_id());
println!("Logs: {}", log_path.display());

if matches!(runtime, RuntimeType::Tmux) {
if matches!(runtime, RuntimeType::Tmux | RuntimeType::Zellij) {
println!("Attach with: runbox attach {}", run.short_id());
}

Expand Down
98 changes: 96 additions & 2 deletions crates/runbox-cli/tests/attach.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ fn test_attach_background_not_supported() {
.failure()
.stderr(
predicate::str::contains("not support")
.or(predicate::str::contains("only supported for tmux")),
.or(predicate::str::contains("only supported for tmux/zellij")),
);
}

Expand Down Expand Up @@ -107,7 +107,7 @@ fn test_attach_no_runtime_not_supported() {
.args(["attach", "b2c3d4e5"])
.assert()
.failure()
.stderr(predicate::str::contains("only supported for tmux"));
.stderr(predicate::str::contains("only supported for tmux/zellij"));
}

/// Feature-gated test for tmux attach functionality.
Expand Down Expand Up @@ -199,3 +199,97 @@ fn test_attach_tmux() {
.args(["kill-session", "-t", &session_name])
.status();
}

/// Feature-gated test for zellij attach functionality.
/// This test requires zellij to be installed and available.
/// Run with: cargo test --features zellij-tests
#[cfg(feature = "zellij-tests")]
#[test]
fn test_attach_zellij() {
use std::process::Command as StdCommand;

let zellij_check = StdCommand::new("zellij").arg("--version").output();
if zellij_check.is_err() || !zellij_check.unwrap().status.success() {
eprintln!("Skipping zellij test: zellij not available");
return;
}

let temp = TempDir::new().unwrap();
let runs_dir = temp.path().join("runs");
fs::create_dir_all(&runs_dir).unwrap();

let session_name = format!("runbox_test_{}", std::process::id());
let tab_name = "test_tab";

let session_result = StdCommand::new("zellij")
.args(["attach", &session_name, "--create-background"])
.status();

if session_result.is_err() || !session_result.unwrap().success() {
eprintln!("Failed to create zellij session");
return;
}

let tab_result = StdCommand::new("zellij")
.args([
"--session",
&session_name,
"action",
"new-tab",
"--name",
tab_name,
])
.status();

if tab_result.is_err() || !tab_result.unwrap().success() {
eprintln!("Failed to create zellij tab");
let _ = StdCommand::new("zellij")
.args(["kill-session", &session_name])
.status();
return;
}

let run = format!(
r#"{{
"run_version": 0,
"run_id": "run_d4e5f607-8901-2345-def0-123456789012",
"exec": {{
"argv": ["echo", "hello"],
"cwd": ".",
"env": {{}},
"timeout_sec": 0
}},
"code_state": {{
"repo_url": "git@github.com:org/repo.git",
"base_commit": "a1b2c3d4e5f6789012345678901234567890abcd"
}},
"status": "running",
"runtime": "zellij",
"handle": {{
"type": "Zellij",
"session": "{}",
"tab": "{}"
}}
}}"#,
session_name, tab_name
);

fs::write(
runs_dir.join("run_d4e5f607-8901-2345-def0-123456789012.json"),
run,
)
.unwrap();

Command::cargo_bin("runbox")
.unwrap()
.env("RUNBOX_HOME", temp.path())
.env_remove("ZELLIJ")
.args(["attach", "d4e5f607"])
.assert()
.failure()
.stderr(predicate::str::contains("terminal"));

let _ = StdCommand::new("zellij")
.args(["kill-session", &session_name])
.status();
}
2 changes: 1 addition & 1 deletion crates/runbox-core/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ impl Index {
let started_at_str: Option<String> = row.get(6)?;
let ended_at_str: Option<String> = row.get(7)?;
let log_path_str: Option<String> = row.get(9)?;

Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
Expand Down
2 changes: 1 addition & 1 deletion crates/runbox-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub use run::{CodeState, Exec, LogRef, Patch, Run, RunStatus, RuntimeHandle, Tim
pub use runnable::{
format_ambiguous_matches, ResolveResult, Runnable, RunnableMatch, RunnableType,
};
pub use runtime::{BackgroundAdapter, RuntimeAdapter, RuntimeRegistry, TmuxAdapter};
pub use runtime::{BackgroundAdapter, RuntimeAdapter, RuntimeRegistry, TmuxAdapter, ZellijAdapter};
pub use skill::{
find_skill_by_name, find_skills, ExportResult, Platform, Skill, SkillError, SkillMetadata,
};
Expand Down
Loading