use anyhow::Result;
use std::path::{Path, PathBuf};
use crate::messaging::slugify_string;
use super::detection::{find_project_root, is_unsafe_pin_location, TRUSTY_TOOLS_DIR};
pub use trusty_common::palace_resolve::{ProjectPin, PIN_FILE_REL, PIN_SCHEMA_VERSION};
pub fn read_project_pin(root: &Path) -> Result<Option<ProjectPin>> {
trusty_common::palace_resolve::read_project_pin(root).map_err(|e| anyhow::anyhow!("{e}"))
}
pub fn write_project_pin(root: &Path, pin: &ProjectPin) -> Result<PathBuf> {
let dir = root.join(TRUSTY_TOOLS_DIR);
std::fs::create_dir_all(&dir).map_err(|e| anyhow::anyhow!("create {}: {e}", dir.display()))?;
let pin_path = root.join(PIN_FILE_REL);
let tmp_path = pin_path.with_extension("yaml.tmp");
let yaml = serde_yaml::to_string(pin).map_err(|e| anyhow::anyhow!("serialise pin: {e}"))?;
let header = "# .trusty-tools/trusty-memory.yaml\n\
# This file pins the trusty-memory palace slug for this project.\n\
# Commit it so the linkage survives directory renames and drive reorgs.\n\
# Schema: https://github.com/bobmatnyc/trusty-tools (trusty-tools convention)\n\n";
let content = format!("{header}{yaml}");
std::fs::write(&tmp_path, &content)
.map_err(|e| anyhow::anyhow!("write {}: {e}", tmp_path.display()))?;
std::fs::rename(&tmp_path, &pin_path).map_err(|e| {
anyhow::anyhow!(
"rename {} → {}: {e}",
tmp_path.display(),
pin_path.display()
)
})?;
Ok(pin_path)
}
pub fn project_slug_from_basename(root: &Path) -> Option<String> {
let basename = root.file_name()?.to_str()?;
let slug = trusty_common::palace_id::clamp_palace_id(&slugify_string(basename));
if slug.is_empty() {
None
} else {
Some(slug)
}
}
fn pin_outcome_at(start: &Path) -> Result<Option<String>> {
let Some(root) = find_project_root(start) else {
return Ok(None);
};
match read_project_pin(&root)? {
Some(pin) if !pin.palace.trim().is_empty() => Ok(Some(pin.palace)),
Some(_) => Err(anyhow::anyhow!(
"palace pin {} has an empty `palace` field",
root.join(PIN_FILE_REL).display()
)),
None => Ok(None),
}
}
pub fn project_slug_at(start: &Path) -> Option<String> {
let root = find_project_root(start)?;
match pin_outcome_at(start) {
Ok(Some(slug)) => return Some(slug),
Ok(None) => {}
Err(e) => {
tracing::warn!(
path = %root.join(PIN_FILE_REL).display(),
"palace pin exists but cannot be trusted ({e:#}); refusing to derive past it"
);
return None;
}
}
let slug = project_slug_from_basename(&root)?;
if is_unsafe_pin_location(&root) {
tracing::debug!(
slug = %slug,
root = %root.display(),
"skipping lazy pin write: root is a system/home/temp dir"
);
return Some(slug);
}
let pin = ProjectPin::new(slug.clone());
match write_project_pin(&root, &pin) {
Ok(path) => tracing::debug!(
slug = %slug,
path = %path.display(),
"wrote palace pin file (lazy init)"
),
Err(e) => tracing::warn!(
slug = %slug,
root = %root.display(),
"could not write palace pin file ({e:#}); slug will remain basename-derived"
),
}
Some(slug)
}
pub fn pinned_slug_at(start: &Path) -> Option<String> {
match pin_outcome_at(start) {
Ok(slug) => slug,
Err(e) => {
tracing::warn!("palace pin exists but cannot be trusted ({e:#}); ignoring");
None
}
}
}
pub fn project_slug_at_readonly(start: &Path) -> Option<String> {
let root = find_project_root(start)?;
match pin_outcome_at(start) {
Ok(Some(slug)) => Some(slug),
Ok(None) => project_slug_from_basename(&root),
Err(e) => {
tracing::warn!(
path = %root.join(PIN_FILE_REL).display(),
"palace pin exists but cannot be trusted ({e:#}); refusing to derive past it"
);
None
}
}
}
pub fn project_slug() -> Result<Option<String>> {
let cwd = std::env::current_dir().map_err(|e| anyhow::anyhow!("read cwd: {e}"))?;
Ok(project_slug_at(&cwd))
}