use std::process;
use crate::HOW_IT_WORKS_URL;
pub fn run_suggest(command: &str) -> ! {
use safe_chains::suggest::{self, Outcome};
const DOCS: &str = "https://www.michaeldhopkins.com/docs/safe-chains/custom-commands.html";
match suggest::analyze(command) {
Outcome::AlreadyAllowed => {
println!("safe-chains already auto-approves this command. Nothing to add.");
process::exit(0);
}
Outcome::Unparseable => {
eprintln!(
"safe-chains couldn't parse this command, so a command definition can't help. \
Check the quoting."
);
process::exit(1);
}
Outcome::RecognizedButDenied { names } => {
match safe_chains::workspace_overreach(command) {
Some((path, reason)) => eprintln!(
"Every command here is one safe-chains already recognizes ({}). It isn't \
auto-approving because of the PATH it reaches, not because the command is \
unknown, so --suggest won't generate an override: {}. {HOW_IT_WORKS_URL}",
names.join(", "),
reason.message(&path)
),
None => eprintln!(
"Every command here is one safe-chains already recognizes ({}). It isn't \
auto-approving because of HOW it's used — a flag or subcommand — not because \
the command is unknown, so --suggest won't generate an override. See {DOCS}.",
names.join(", ")
),
}
process::exit(1);
}
Outcome::Generated { entries, also_recognized } => {
emit_suggestion(&entries, &also_recognized);
}
}
}
fn repo_config_path() -> std::path::PathBuf {
let Ok(start) = std::env::current_dir() else {
return std::path::PathBuf::from(REPO_FILENAME);
};
repo_config_path_from(&start)
}
const REPO_FILENAME: &str = ".safe-chains.toml";
fn repo_config_path_from(start: &std::path::Path) -> std::path::PathBuf {
let mut dir = start.to_path_buf();
let root = loop {
if dir.join(".git").exists() || dir.join(".jj").exists() {
break Some(dir);
}
if !dir.pop() {
break None;
}
};
let Some(root) = root else {
return start.join(REPO_FILENAME);
};
let mut dir = start.to_path_buf();
loop {
let candidate = dir.join(REPO_FILENAME);
if candidate.is_file() {
return candidate;
}
if dir == root || !dir.pop() {
break;
}
}
root.join(REPO_FILENAME)
}
fn emit_suggestion(entries: &[safe_chains::suggest::GeneratedEntry], also_recognized: &[String]) -> ! {
use safe_chains::suggest;
let target = repo_config_path();
let existing = std::fs::read_to_string(&target).unwrap_or_default();
let merged = suggest::merged_content(&existing, entries);
let hash = suggest::config_hash(merged.as_bytes());
let block = suggest::render_toml(entries);
let dir = target.parent().unwrap_or_else(|| std::path::Path::new("."));
let canonical = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
let pin = suggest::pin_block(&canonical.to_string_lossy(), &hash);
let shown = safe_chains::sanitize_display(&target.display().to_string());
if !existing.trim().is_empty()
&& let Err(e) = toml::from_str::<toml::Value>(&existing)
{
eprintln!(
"{shown} isn't valid TOML ({e}), so adding to it would leave a file safe-chains can't \
load. Fix or move that file, then re-run. The block to add is:\n\n{block}"
);
process::exit(1);
}
println!("Add this to {shown}:\n\n{block}");
println!(
"That file does nothing until you approve it. Add this to ~/.config/safe-chains.toml \
(which safe-chains never edits):\n\n{pin}"
);
println!(
"The level defaults to \"SafeWrite\". Edit it to \"SafeRead\" (runs code, no \
artifacts) or \"Inert\" (read-only) if that fits the tool. The hash above is of {shown} \
with exactly the block above added; if you change either, recompute it with \
`shasum -a 256 {shown}` and update the pin."
);
if !also_recognized.is_empty() {
println!(
"\nHeads up: this command also uses commands safe-chains already recognizes \
({}). The entry above only covers the unrecognized one(s), so if the whole \
command still isn't approved, one of those is why.",
also_recognized.join(", ")
);
}
process::exit(0);
}