use super::*;
pub(super) enum NamedEntry {
Absent,
Importable(fs::Metadata),
Rejected(String),
}
#[derive(Clone, Copy)]
pub(super) struct NamedSessionStore {
harness: &'static str,
stored: &'static str,
}
pub(super) const CLAUDE_STORE: NamedSessionStore = NamedSessionStore {
harness: "Claude",
stored: "transcripts",
};
pub(super) const CODEX_STORE: NamedSessionStore = NamedSessionStore {
harness: "Codex",
stored: "rollouts",
};
pub(super) const KIMI_STORE: NamedSessionStore = NamedSessionStore {
harness: "Kimi",
stored: "session directories",
};
pub(super) const GROK_STORE: NamedSessionStore = NamedSessionStore {
harness: "Grok Build",
stored: "session directories",
};
impl NamedSessionStore {
pub fn file(self, path: &Path) -> NamedEntry {
self.classify(path, false)
}
pub fn directory(self, path: &Path) -> NamedEntry {
self.classify(path, true)
}
pub fn container(self, path: &Path, described_as: &str) -> NamedEntry {
let metadata = match entry_metadata(path) {
NamedEntry::Importable(metadata) => metadata,
unreadable => return unreadable,
};
if metadata.file_type().is_symlink() {
return NamedEntry::Rejected(self.symlinked_container(path, described_as));
}
if !metadata.is_dir() {
return NamedEntry::Absent;
}
NamedEntry::Importable(metadata)
}
fn classify(self, path: &Path, want_directory: bool) -> NamedEntry {
let metadata = match entry_metadata(path) {
NamedEntry::Importable(metadata) => metadata,
unreadable => return unreadable,
};
if metadata.file_type().is_symlink() {
return NamedEntry::Rejected(format!(
"{} is a symlink; {}",
path.display(),
self.only_stored_directly()
));
}
if want_directory {
if !metadata.is_dir() {
return NamedEntry::Rejected(format!("{} is not a directory", path.display()));
}
} else if !metadata.is_file() {
return NamedEntry::Rejected(format!("{} is not a regular file", path.display()));
}
NamedEntry::Importable(metadata)
}
pub fn symlinked_container(self, path: &Path, described_as: &str) -> String {
format!(
"{} is a symlinked {described_as}; {}",
path.display(),
self.only_stored_directly()
)
}
pub fn no_cwd(self, path: &Path) -> String {
format!("{} session {} has no cwd", self.harness, path.display())
}
pub fn cannot_import(self, native_session_id: &str, rejected: &[String]) -> anyhow::Error {
anyhow::anyhow!(
"{} session {native_session_id:?} cannot be imported: {}",
self.harness,
rejected.join("; ")
)
}
fn only_stored_directly(self) -> String {
format!(
"Mjolnir imports only {} stored directly in the {} home",
self.stored, self.harness
)
}
}
fn entry_metadata(path: &Path) -> NamedEntry {
match fs::symlink_metadata(path) {
Ok(metadata) => NamedEntry::Importable(metadata),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => NamedEntry::Absent,
Err(error) => NamedEntry::Rejected(cannot_read(path, &error)),
}
}
pub(super) fn cannot_read(path: &Path, error: &std::io::Error) -> String {
format!("{} cannot be read: {error}", path.display())
}