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
//! Session environment setup.
//!
//! Initializes the runtime environment at session start:
//! - Detects project root (git root or cwd)
//! - Loads memory context
//! - Sets up safe environment variables
//! - Configures signal handlers
//! - Prepares the session state
use std::path::{Path, PathBuf};
/// Resolved session environment.
#[derive(Debug, Clone)]
pub struct SessionEnvironment {
/// Original working directory at startup.
pub original_cwd: PathBuf,
/// Detected project root (git root or cwd).
pub project_root: PathBuf,
/// Whether we're inside a git repository.
pub is_git_repo: bool,
/// Current git branch (if in a repo).
pub git_branch: Option<String>,
/// Platform identifier.
pub platform: String,
/// Default shell.
pub shell: String,
/// Whether the session is interactive (has a TTY).
pub is_interactive: bool,
/// Terminal width in columns.
pub terminal_width: u16,
}
impl SessionEnvironment {
/// Detect and build the session environment.
pub async fn detect() -> Self {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let project_root = detect_project_root(&cwd).await;
let is_git = crate::services::git::is_git_repo(&cwd).await;
let branch = if is_git {
crate::services::git::current_branch(&cwd).await
} else {
None
};
let terminal_width = crossterm::terminal::size().map(|(w, _)| w).unwrap_or(80);
Self {
original_cwd: cwd,
project_root,
is_git_repo: is_git,
git_branch: branch,
platform: std::env::consts::OS.to_string(),
shell: detect_shell(),
is_interactive: atty_check(),
terminal_width,
}
}
}
/// Walk up from cwd to find the project root (git root or first dir with config).
async fn detect_project_root(cwd: &Path) -> PathBuf {
// Try git root first.
if let Some(root) = crate::services::git::repo_root(cwd).await {
return PathBuf::from(root);
}
// Look for project markers.
let markers = [
".rc",
"Cargo.toml",
"package.json",
"pyproject.toml",
"go.mod",
];
let mut dir = cwd.to_path_buf();
loop {
for marker in &markers {
if dir.join(marker).exists() {
return dir;
}
}
if !dir.pop() {
break;
}
}
cwd.to_path_buf()
}
fn detect_shell() -> String {
std::env::var("SHELL").unwrap_or_else(|_| "bash".to_string())
}
fn atty_check() -> bool {
#[cfg(unix)]
{
// SAFETY: isatty is a standard POSIX function with no preconditions.
unsafe { libc::isatty(0) != 0 }
}
#[cfg(not(unix))]
{
true
}
}