mod approval;
mod cli;
mod client;
mod distro_routing;
mod error;
mod mcp_server;
mod operator_text;
mod render;
mod runner;
mod unattended;
use clap::Parser;
use crate::cli::{Cli, Command};
use crate::runner::{Logger, RunOpts};
fn main() {
let cli = Cli::parse();
sysknife_core::config::LacsConfig::load().apply_defaults_to_env();
let socket = runner::resolve_socket_target();
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
eprintln!("sysknife: could not start the async runtime: {e}");
std::process::exit(4);
}
};
runtime.block_on(run(cli, socket));
}
async fn run(cli: Cli, socket: crate::client::SocketTarget) {
let log = match Logger::new(cli.log_to.as_deref()) {
Ok(l) => l,
Err(e) => {
eprintln!("sysknife: {e}");
std::process::exit(4);
}
};
let result: Result<(), crate::error::CliError> = async {
if let Some(secs) = cli.timeout {
tokio::time::timeout(
std::time::Duration::from_secs(secs),
dispatch(&cli, socket, &log),
)
.await
.unwrap_or(Err(crate::error::CliError::TimedOut(secs)))
} else {
dispatch(&cli, socket, &log).await
}
}
.await;
if let Err(e) = result {
match &e {
crate::error::CliError::Exit(_) => {}
crate::error::CliError::Refused { reason, suggestion } => {
eprintln!("Cannot satisfy that request.");
eprintln!(" {}", crate::operator_text::operator_safe(reason));
if let Some(s) = suggestion {
eprintln!(" Try: {}", crate::operator_text::operator_safe(s));
}
}
_ => eprintln!("sysknife: {e}"),
}
std::process::exit(e.exit_code());
}
}
async fn dispatch(
cli: &Cli,
socket: crate::client::SocketTarget,
log: &Logger,
) -> Result<(), crate::error::CliError> {
let consent = crate::cli::UnattendedConsent::from_env(cli.dangerously_skip_approval);
if let Some(refusal) = consent.refusal() {
return Err(crate::error::CliError::UnattendedConsentMissing(refusal));
}
if consent.is_granted() && !cli.dry_run {
crate::unattended::warn_and_pause().await;
}
match &cli.command {
Some(Command::Completions { shell }) => {
runner::run_completions(*shell);
Ok(())
}
Some(Command::Doctor) => runner::run_doctor(socket, cli.json, log).await,
Some(Command::History(args)) => runner::run_history(args.clone(), socket, log).await,
Some(Command::Approve {
transaction_id,
full,
}) => {
let opts = build_run_opts(cli, socket);
runner::run_approve(
&sysknife_types::TransactionId::new(transaction_id.clone()),
&opts,
*full,
log,
)
.await
}
Some(Command::McpServer) => mcp_server::run_mcp_server().await,
Some(Command::Audit { command }) => match command {
crate::cli::AuditCommand::Export(args) => {
runner::run_audit_export(args.clone(), log).await
}
crate::cli::AuditCommand::Verify(args) => {
runner::run_audit_verify(args.clone(), log).await
}
crate::cli::AuditCommand::Checkpoint(args) => {
runner::run_audit_checkpoint(args.clone(), log).await
}
},
Some(Command::Intent(_)) => {
let intent = cli
.command
.as_ref()
.unwrap()
.intent_string()
.expect("Intent variant always has a string");
let opts = build_run_opts(cli, socket);
runner::run_intent(intent, &opts, log).await
}
None => {
let opts = build_run_opts(cli, socket);
runner::run_repl(&opts, log).await
}
}
}
fn build_run_opts(cli: &Cli, socket: crate::client::SocketTarget) -> RunOpts {
RunOpts {
socket,
yes: cli.yes,
max_risk: cli.max_risk.map(crate::approval::MaxRisk::from),
non_interactive: cli.non_interactive,
dry_run: cli.dry_run,
json: cli.json,
step_by_step: cli.step_by_step,
skip_approval: crate::cli::UnattendedConsent::from_env(cli.dangerously_skip_approval)
.is_granted(),
}
}