1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//! Cross-platform login shell command builder.
//!
//! Provides a helper to construct shell commands that execute via the user's
//! login shell (`$SHELL -l -c`) on Unix, ensuring PATH and environment variables
//! from `.zprofile`/`.profile` are available even when cflx is started from
//! non-login environments (launchd, systemd, cron).
//!
//! On Windows, commands use `cmd /C` as before.
use tracing::debug;
/// Build a [`tokio::process::Command`] that runs `command_str` via the user's
/// login shell on Unix (`$SHELL -l -c`) or `cmd /C` on Windows.
///
/// The returned command:
/// - Inherits the current process environment (`env_clear()` + `envs(std::env::vars())`)
/// - Sets `stdin` to null (no interactive input)
/// - Does **not** set `stdout`/`stderr` – callers should configure capture/piping as needed
///
/// # Examples
///
/// ```ignore
/// let mut cmd = shell_command::build_login_shell_command("opencode run");
/// cmd.current_dir(work_dir);
/// cmd.stdout(std::process::Stdio::piped());
/// let status = cmd.status().await?;
/// ```
pub fn build_login_shell_command(command_str: &str) -> tokio::process::Command {
if cfg!(target_os = "windows") {
debug!(
"Building login shell command (Windows): cmd /C {}",
command_str
);
let mut cmd = tokio::process::Command::new("cmd");
cmd.arg("/C")
.arg(command_str)
.env_clear()
.envs(std::env::vars())
.stdin(std::process::Stdio::null());
cmd
} else {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
debug!(
"Building login shell command (Unix): {} -l -c {}",
shell, command_str
);
let mut cmd = tokio::process::Command::new(&shell);
cmd.arg("-l")
.arg("-c")
.arg(command_str)
.env_clear()
.envs(std::env::vars())
.stdin(std::process::Stdio::null());
cmd
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_build_login_shell_command_runs_echo() {
let mut cmd = build_login_shell_command("echo hello");
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let output = cmd.output().await.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("hello"),
"Expected 'hello' in stdout, got: {}",
stdout
);
}
#[tokio::test]
async fn test_build_login_shell_command_inherits_env() {
// Set a custom env var and verify it's visible in the child
// SAFETY: This test is single-threaded and the env var is immediately cleaned up.
unsafe {
std::env::set_var("CFLX_TEST_MARKER", "login_shell_test");
}
let mut cmd = build_login_shell_command("echo $CFLX_TEST_MARKER");
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let output = cmd.output().await.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("login_shell_test"),
"Expected env var value in stdout, got: {}",
stdout
);
// SAFETY: Cleanup env var set above.
unsafe {
std::env::remove_var("CFLX_TEST_MARKER");
}
}
#[cfg(unix)]
#[tokio::test]
async fn test_build_login_shell_command_uses_login_shell() {
// Verify that the command is executed via $SHELL -l -c by checking
// that PATH from login profile is available.
// We test this by running a command that simply exits 0.
let mut cmd = build_login_shell_command("true");
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let output = cmd.output().await.expect("Failed to execute command");
assert!(
output.status.success(),
"Login shell command should succeed"
);
}
}