use super::*;
pub(crate) fn first_slug_record(bytes: &[u8]) -> Option<(usize, crate::model::Record)> {
static SLUG: std::sync::LazyLock<memchr::memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b"\"slug\""));
let mut line_no = 0usize;
for line in bytes.split(|&b| b == b'\n') {
line_no += 1;
if SLUG.find(line).is_none() {
continue;
}
if let Ok(Some(rec)) = crate::parse::parse_line(line) {
if rec.slug.is_some() {
return Some((line_no, rec));
}
}
}
None
}
pub(crate) fn slug_is_valid(s: &str) -> bool {
let mut chars = s.chars();
let Some(head) = chars.next() else {
return false;
};
s.len() <= 120
&& (head.is_ascii_lowercase() || head.is_ascii_digit())
&& chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
pub(crate) fn first_cwd(bytes: &[u8]) -> Option<String> {
let mut pos = 0usize;
let mut seen = 0usize;
while pos < bytes.len() && seen < 256 {
let end = memchr::memchr(b'\n', &bytes[pos..]).map_or(bytes.len(), |i| pos + i);
let line = &bytes[pos..end];
pos = end + 1;
seen += 1;
if let Ok(Some(rec)) = crate::parse::parse_line(line) {
if let Some(c) = rec.cwd.as_deref().filter(|c| !c.is_empty()) {
return Some(c.to_string());
}
}
}
None
}
pub(crate) fn plans_dir(project_root: Option<&Path>) -> PathBuf {
let Ok(home) = crate::path::claude_home() else {
return PathBuf::from("plans");
};
let default = home.join("plans");
let Some(root) = project_root else {
return default;
};
let mut candidates: Vec<PathBuf> = vec![home.join("settings.json")];
candidates.push(root.join(".claude").join("settings.json"));
candidates.push(root.join(".claude").join("settings.local.json"));
let mut value: Option<String> = None;
for p in candidates {
let Ok(raw) = std::fs::read_to_string(&p) else {
continue;
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
continue;
};
if let Some(d) = v.get("plansDirectory").and_then(serde_json::Value::as_str) {
value = Some(d.to_string()); }
}
let Some(d) = value else {
return default;
};
let joined = crate::path::lexical_normalize(&root.join(d));
let root_n = crate::path::lexical_normalize(root);
if joined.starts_with(&root_n) {
joined
} else {
default
}
}