use crate::*;
pub(crate) fn cmd_state(args: &[String]) -> i32 {
let root = args.first().map(String::as_str).unwrap_or(".");
let mut files: Vec<PathBuf> = Vec::new();
collect_rs(Path::new(root), &mut files);
files.sort();
let mut h: u64 = 0xcbf2_9ce4_8422_2325; let mut feed = |bytes: &[u8]| {
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
};
for f in &files {
let rel = f.strip_prefix(root).unwrap_or(f);
feed(rel.to_string_lossy().as_bytes());
feed(&[0]);
if let Ok(bytes) = std::fs::read(f) {
feed(&bytes);
}
feed(&[0]);
}
println!("{h:016x}");
0
}
pub(crate) fn clear_other_reports(prefix: &str, keep: &str) -> usize {
let p = Path::new(prefix);
let dir = p.parent().filter(|d| !d.as_os_str().is_empty()).map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."));
let Some(base) = p.file_name().and_then(|s| s.to_str()) else { return 0 };
let prefix_dot = format!("{base}.");
let Ok(rd) = std::fs::read_dir(&dir) else { return 0 };
let mut removed = 0;
for ent in rd.flatten() {
let name = ent.file_name();
let Some(name) = name.to_str() else { continue };
if !name.starts_with(&prefix_dot) || !name.ends_with(".json") {
continue;
}
let scan = is_scan_artifact(base, name);
let remove = match keep {
"scan" => !scan, "lint" => scan, _ => false,
};
if remove && std::fs::remove_file(ent.path()).is_ok() {
removed += 1;
}
}
removed
}
pub(crate) fn cmd_reports(args: &[String]) -> i32 {
let exists_only = args.iter().any(|a| a == "--exists");
let backend = args.iter().any(|a| a == "--backend");
let clear_keep = args.iter().position(|a| a == "--clear-other").and_then(|i| args.get(i + 1)).cloned();
let Some(prefix) = args.iter().find(|a| !a.starts_with("--")) else {
eprintln!("usage: candor-query reports <prefix> [--exists | --backend | --clear-other <scan|lint>]");
return 2;
};
if backend {
println!("{}", report_backend(prefix));
return 0;
}
if let Some(keep) = clear_keep {
clear_other_reports(prefix, &keep);
return 0;
}
let files = report_files(prefix);
if exists_only {
return if files.is_empty() { 1 } else { 0 };
}
for rf in &files {
println!("{}", rf.path.display());
}
0
}
pub(crate) fn locate_newest(kind: &str, dirs: &[String]) -> Option<PathBuf> {
let matches_kind = |name: &str| -> bool {
match kind {
"scan" => name == "candor-scan",
"lib" => name.starts_with("libcandor@") && (name.ends_with(".dylib") || name.ends_with(".so")),
_ => false,
}
};
let mut newest: Option<(std::time::SystemTime, PathBuf)> = None;
for dir in dirs {
let Ok(rd) = std::fs::read_dir(dir) else { continue };
for ent in rd.flatten() {
let name = ent.file_name();
let Some(name) = name.to_str() else { continue };
if !matches_kind(name) {
continue;
}
let Ok(mtime) = ent.metadata().and_then(|m| m.modified()) else { continue };
if newest.as_ref().is_none_or(|(t, _)| mtime > *t) {
newest = Some((mtime, ent.path()));
}
}
}
newest.map(|(_, p)| p)
}
pub(crate) fn cmd_locate(args: &[String]) -> i32 {
let Some(kind) = args.first() else {
eprintln!("usage: candor-query locate <lib|scan> <dir>...");
return 2;
};
match locate_newest(kind, &args[1..]) {
Some(p) => {
println!("{}", p.display());
0
}
None => 1,
}
}
pub(crate) fn cmd_engine_version(args: &[String]) -> i32 {
let Some(path) = args.first() else {
eprintln!("usage: candor-query engine-version <lib-path>");
return 2;
};
let Ok(bytes) = std::fs::read(path) else { return 1 };
let needle = b"candor-build-version=";
let Some(start) = bytes.windows(needle.len()).position(|w| w == needle) else { return 1 };
let v: Vec<u8> = bytes[start + needle.len()..]
.iter()
.copied()
.take_while(|b| b.is_ascii_alphanumeric())
.collect();
if v.is_empty() {
return 1;
}
println!("{}", String::from_utf8_lossy(&v));
0
}
pub(crate) fn cmd_merge_hook(args: &[String]) -> i32 {
let (Some(path), Some(cmd)) = (args.first(), args.get(1)) else {
eprintln!("usage: candor-query merge-hook <settings.json> <hook-command>");
return 2;
};
let manual = || {
eprintln!(" WARNING: {path} isn't plain JSON (comments or trailing commas?) — NOT modifying it.");
eprintln!(
" Add this Stop hook by hand: {{\"matcher\":\"*\",\"hooks\":[{{\"type\":\"command\",\"command\":\"{cmd}\"}}]}}"
);
};
let mut data: serde_json::Value = if Path::new(path).exists() {
match std::fs::read_to_string(path) {
Ok(s) => match serde_json::from_str(&s) {
Ok(v) => v,
Err(_) => {
manual();
return 0; }
},
Err(e) => {
eprintln!("candor-query: cannot read {path}: {e}");
return 1;
}
}
} else {
serde_json::json!({})
};
let Some(obj) = data.as_object_mut() else {
manual();
return 0;
};
let hooks = obj.entry("hooks").or_insert_with(|| serde_json::json!({}));
let Some(stop) = hooks
.as_object_mut()
.map(|h| h.entry("Stop").or_insert_with(|| serde_json::json!([])))
.and_then(|s| s.as_array_mut())
else {
manual();
return 0;
};
let present = stop.iter().any(|g| {
g.get("hooks").and_then(|h| h.as_array()).is_some_and(|hs| {
hs.iter().any(|h| h.get("command").and_then(|c| c.as_str()) == Some(cmd.as_str()))
})
});
if present {
println!(" Stop hook already present in {path}");
return 0;
}
stop.push(serde_json::json!({"matcher": "*", "hooks": [{"type": "command", "command": cmd}]}));
let body = serde_json::to_string_pretty(&data).unwrap_or_default();
if let Err(e) = std::fs::write(path, format!("{body}\n")) {
eprintln!("candor-query: cannot write {path}: {e}");
return 1;
}
println!(" merged Stop hook into {path}");
0
}
pub(crate) fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for ent in rd.flatten() {
let path = ent.path();
let Ok(ft) = ent.file_type() else { continue };
if ft.is_dir() {
let name = ent.file_name();
if name == "target" || name == ".git" {
continue;
}
collect_rs(&path, out);
} else if ft.is_file() && path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}