use std::io::Read;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::Parser;
use coding_tools::cli::ct_steer::{CheckArgs, Cli, Command, HookArgs, InstallArgs};
use coding_tools::explain::Format;
use coding_tools::pulse::{self, PulseState};
use coding_tools::steer::{self, install};
const EXPLAIN_MD: &str = include_str!("../../docs/explain/ct-steer.md");
const EXPLAIN_JSON: &str = include_str!("../../docs/explain/ct-steer.json");
fn home_dir() -> Result<PathBuf, String> {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
.ok_or_else(|| "cannot find your home directory (HOME / USERPROFILE unset)".to_string())
}
fn cmd_hook(args: &HookArgs) -> ExitCode {
let mut envelope = String::new();
if std::io::stdin().read_to_string(&mut envelope).is_err() {
return ExitCode::SUCCESS; }
if let Some(decision) = steer::hook::process(&envelope, args.mode.to_lib()) {
println!("{decision}");
}
ExitCode::SUCCESS
}
fn settings_path(args: &InstallArgs) -> Result<PathBuf, String> {
let root = std::env::current_dir().map_err(|e| format!("cannot read current dir: {e}"))?;
let home = match args.scope {
coding_tools::cli::ct_steer::Scope::User => home_dir()?,
_ => root.clone(),
};
Ok(args.scope.to_lib().path(&root, &home))
}
fn cmd_install(cli: &Cli, args: &InstallArgs) -> Result<ExitCode, String> {
let command = install::hook_command(args.mode.to_lib());
if args.print {
let (snippet, _) = install::install(None, &command)?;
print!("{snippet}");
return Ok(ExitCode::SUCCESS);
}
let path = settings_path(args)?;
let existing = read_settings(&path)?;
let (text, changed) = install::install(existing.as_deref(), &command)?;
if args.dry_run {
if !cli.quiet {
eprintln!("# would write {}", path.display());
}
print!("{text}");
return Ok(ExitCode::SUCCESS);
}
write_settings(&path, &text)?;
report(cli, &path, changed, "installed", "already present");
Ok(ExitCode::SUCCESS)
}
fn cmd_uninstall(cli: &Cli, args: &InstallArgs) -> Result<ExitCode, String> {
let path = settings_path(args)?;
let Some(existing) = read_settings(&path)? else {
report(cli, &path, false, "removed", "no settings file");
return Ok(ExitCode::SUCCESS);
};
let (text, changed) = install::uninstall(Some(&existing))?;
if args.dry_run {
print!("{text}");
return Ok(ExitCode::SUCCESS);
}
if changed {
write_settings(&path, &text)?;
}
report(cli, &path, changed, "removed", "not present");
Ok(ExitCode::SUCCESS)
}
fn cmd_check(cli: &Cli, args: &CheckArgs) -> ExitCode {
let mode = args.mode.to_lib();
match steer::analyze(&args.command) {
None => {
if cli.json {
println!("{}", serde_json::json!({ "decision": "allow" }));
} else if !cli.quiet {
println!("ALLOW — no ct tool clearly fits this command");
}
ExitCode::SUCCESS
}
Some(s) => {
if cli.json {
println!("{}", steer::hook::decision(&s, mode));
} else if !cli.quiet {
println!("{} [{}] — {}", mode_label(mode), s.rule_id, s.tool);
println!(" {}", s.suggestion);
println!("({})", s.note);
}
ExitCode::from(1)
}
}
}
fn mode_label(mode: steer::Mode) -> &'static str {
match mode {
steer::Mode::Deny => "DENY",
steer::Mode::Ask => "ASK",
steer::Mode::Warn => "WARN",
}
}
fn read_settings(path: &std::path::Path) -> Result<Option<String>, String> {
match std::fs::read_to_string(path) {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!("read {}: {e}", path.display())),
}
}
fn write_settings(path: &std::path::Path, text: &str) -> Result<(), String> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
}
std::fs::write(path, text).map_err(|e| format!("write {}: {e}", path.display()))
}
fn report(cli: &Cli, path: &std::path::Path, changed: bool, did: &str, noop: &str) {
if cli.json {
println!(
"{}",
serde_json::json!({ "path": path.display().to_string(), "changed": changed })
);
return;
}
if cli.quiet {
return;
}
if changed {
println!("ct steer hook {did} in {}", path.display());
} else {
println!("ct steer hook {noop} ({})", path.display());
}
}
fn run(cli: Cli) -> Result<ExitCode, String> {
let _watchdog = pulse::watchdog("ct-steer", cli.timeout)?;
let _pulse = cli.heartbeat.start("ct-steer", PulseState::new())?;
let Some(command) = &cli.command else {
return Err("specify a subcommand (hook, install, uninstall, check — see `ct-steer --help`)".to_string());
};
match command {
Command::Hook(a) => Ok(cmd_hook(a)),
Command::Install(a) => cmd_install(&cli, a),
Command::Uninstall(a) => cmd_uninstall(&cli, a),
Command::Check(a) => Ok(cmd_check(&cli, a)),
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
if let Some(fmt) = cli.explain {
let body = match fmt {
Format::Md => EXPLAIN_MD,
Format::Json => EXPLAIN_JSON,
};
print!("{body}");
return ExitCode::SUCCESS;
}
match run(cli) {
Ok(code) => code,
Err(msg) => {
eprintln!("ct-steer: {msg}");
ExitCode::from(2)
}
}
}