use std::path::{Path, PathBuf};
pub fn resolve_palace(explicit: Option<&str>) -> String {
if let Some(p) = explicit {
return p.to_string();
}
if let Ok(cwd) = std::env::current_dir() {
if let Some(name) = find_project_root(&cwd) {
return to_palace_id(&name);
}
}
"default".to_string()
}
pub fn detect_serve_palace(explicit: Option<&str>) -> Option<String> {
if let Some(p) = explicit {
return Some(to_palace_id(p));
}
let cwd = std::env::current_dir().ok()?;
if let Some(name) = read_marker_palace(&cwd) {
let id = to_palace_id(&name);
if !id.is_empty() {
return Some(id);
}
}
let name = cwd.file_name()?.to_string_lossy().into_owned();
let id = to_palace_id(&name);
if id.is_empty() {
None
} else {
Some(id)
}
}
fn read_marker_palace(start: &Path) -> Option<String> {
let mut dir = start.to_path_buf();
loop {
let marker = dir.join(".trusty-memory");
if marker.is_file() {
if let Ok(contents) = std::fs::read_to_string(&marker) {
if let Some(name) = parse_marker(&contents) {
return Some(name);
}
}
}
if !pop_in_place(&mut dir) {
return None;
}
}
}
fn parse_marker(contents: &str) -> Option<String> {
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(value) = line.strip_prefix("palace=") {
let value = value.trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
fn find_project_root(start: &Path) -> Option<String> {
let markers = [".claude", "CLAUDE.md", ".git"];
let mut dir = start.to_path_buf();
loop {
for marker in &markers {
if dir.join(marker).exists() {
return dir.file_name().map(|n| n.to_string_lossy().into_owned());
}
}
if !pop_in_place(&mut dir) {
break;
}
}
None
}
fn pop_in_place(dir: &mut PathBuf) -> bool {
dir.pop()
}
pub fn to_palace_id(name: &str) -> String {
let mapped: String = name
.chars()
.map(|c| {
if c.is_alphanumeric() {
c.to_lowercase().next().unwrap_or(c)
} else {
'-'
}
})
.collect();
mapped.trim_matches('-').to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_palace_id_kebab_cases() {
assert_eq!(to_palace_id("MyProject"), "myproject");
assert_eq!(to_palace_id("trusty_memory"), "trusty-memory");
assert_eq!(to_palace_id("My Cool Project!"), "my-cool-project");
}
#[test]
fn resolve_falls_back_to_default() {
assert_eq!(resolve_palace(Some("my-palace")), "my-palace");
}
#[test]
fn parse_marker_extracts_palace() {
assert_eq!(
parse_marker("# comment\npalace=client-acme\n"),
Some("client-acme".to_string())
);
assert_eq!(
parse_marker(" palace = spaced \npalace= trimmed \n"),
Some("trimmed".to_string())
);
assert_eq!(parse_marker("# only comments\n\n"), None);
assert_eq!(parse_marker("palace=\n"), None);
}
#[test]
fn detect_serve_palace_explicit_override_is_sanitized() {
assert_eq!(
detect_serve_palace(Some("My Project")),
Some("my-project".to_string())
);
}
#[test]
fn detect_serve_palace_reads_marker() {
let dir = std::env::temp_dir().join(format!(
"trusty-marker-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&dir).expect("create temp dir");
std::fs::write(dir.join(".trusty-memory"), "palace=Custom Name\n").expect("write marker");
let found = read_marker_palace(&dir);
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(found, Some("Custom Name".to_string()));
assert_eq!(
found.map(|n| to_palace_id(&n)),
Some("custom-name".to_string())
);
}
}