lean_ctx/tools/
startup.rs1use super::server::LeanCtxServer;
2
3#[derive(Clone, Debug, Default)]
4pub(super) struct StartupContext {
5 pub(super) project_root: Option<String>,
6 pub(super) shell_cwd: Option<String>,
7}
8
9pub fn create_server() -> LeanCtxServer {
11 LeanCtxServer::new()
12}
13
14pub(super) fn has_project_marker(dir: &std::path::Path) -> bool {
15 crate::core::pathutil::has_project_marker(dir)
16}
17
18pub(super) fn is_suspicious_root(dir: &std::path::Path) -> bool {
19 crate::core::pathutil::is_agent_config_dir(dir)
20}
21
22pub(super) fn canonicalize_path(path: &std::path::Path) -> String {
23 crate::core::pathutil::safe_canonicalize_or_self(path)
24 .to_string_lossy()
25 .to_string()
26}
27
28pub(super) fn detect_startup_context(
29 explicit_project_root: Option<&str>,
30 startup_cwd: Option<&std::path::Path>,
31) -> StartupContext {
32 let shell_cwd = startup_cwd.map(canonicalize_path);
33 let project_root = explicit_project_root
34 .map(|root| canonicalize_path(std::path::Path::new(root)))
35 .or_else(|| {
36 startup_cwd
37 .and_then(maybe_derive_project_root_from_absolute)
38 .map(|p| canonicalize_path(&p))
39 });
40
41 let shell_cwd = match (shell_cwd, project_root.as_ref()) {
42 (Some(cwd), Some(root))
43 if std::path::Path::new(&cwd).starts_with(std::path::Path::new(root)) =>
44 {
45 Some(cwd)
46 }
47 (_, Some(root)) => Some(root.clone()),
48 (cwd, None) => cwd,
49 };
50
51 StartupContext {
52 project_root,
53 shell_cwd,
54 }
55}
56
57pub(super) fn maybe_derive_project_root_from_absolute(
58 abs: &std::path::Path,
59) -> Option<std::path::PathBuf> {
60 let mut cur = if abs.is_dir() {
61 abs.to_path_buf()
62 } else {
63 abs.parent()?.to_path_buf()
64 };
65 let mut best: Option<std::path::PathBuf> = None;
70 loop {
71 if has_project_marker(&cur) {
72 best = Some(crate::core::pathutil::safe_canonicalize_or_self(&cur));
73 }
74 if !cur.pop() {
75 break;
76 }
77 }
78 best
79}
80
81pub(crate) fn auto_consolidate_knowledge(project_root: &str) {
87 let _ = crate::tools::ctx_knowledge::consolidate_project_knowledge_with(
88 project_root,
89 &crate::core::consolidation_engine::ConsolidateOptions::incremental_auto(),
90 );
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn derive_root_promotes_to_outermost_marker_in_monorepo() {
99 let tmp = tempfile::tempdir().unwrap();
103 let repo = tmp.path().join("repo");
104 let sub = repo.join("modules").join("payroll");
105 std::fs::create_dir_all(&sub).unwrap();
106 std::fs::create_dir(repo.join(".git")).unwrap();
108 std::fs::write(repo.join("package.json"), "{}").unwrap();
109 std::fs::write(sub.join("package.json"), "{}").unwrap();
111
112 let result = maybe_derive_project_root_from_absolute(&sub);
113 assert!(result.is_some(), "should find a root");
114 let root = result.unwrap();
115 let canonical_repo = crate::core::pathutil::safe_canonicalize_or_self(&repo);
116 assert_eq!(
117 root, canonical_repo,
118 "should return repo root, not sub-package"
119 );
120 }
121
122 #[test]
123 fn derive_root_returns_nearest_when_no_outer_marker() {
124 let tmp = tempfile::tempdir().unwrap();
127 let pkg = tmp.path().join("standalone");
128 std::fs::create_dir_all(&pkg).unwrap();
129 std::fs::write(pkg.join("package.json"), "{}").unwrap();
130
131 let result = maybe_derive_project_root_from_absolute(&pkg);
132 assert!(result.is_some());
133 let root = result.unwrap();
134 let canonical = crate::core::pathutil::safe_canonicalize_or_self(&pkg);
135 assert_eq!(root, canonical);
136 }
137}