use std::io::Write;
use tirith_core::intent::{self, IntentBehaviorReport};
use tirith_core::tokenize::ShellType;
pub fn run(intent: &str, command: &str, explain: bool, json: bool, shell: &str) -> i32 {
let intent = intent.trim();
let command = command.trim();
if intent.is_empty() {
eprintln!(
"tirith intend: no intent given (usage: tirith intend \"install a formatter\" -- \"<command>\")"
);
return 2;
}
if command.is_empty() {
eprintln!(
"tirith intend: no command given (usage: tirith intend \"<intent>\" -- \"curl https://x/install.sh | bash\")"
);
return 2;
}
let shell_type = shell.parse::<ShellType>().unwrap_or_else(|_| {
eprintln!("tirith intend: unknown shell '{shell}', falling back to posix");
ShellType::Posix
});
let report = intent::analyze_intent(intent, command, shell_type, explain);
if json {
return emit_json(intent, command, &report, explain);
}
print_human(intent, command, &report, explain);
if report.has_mismatch() {
1
} else {
0
}
}
fn print_human(intent: &str, command: &str, report: &IntentBehaviorReport, explain: bool) {
let intent = super::sanitize_for_human_output(intent, false);
let command = super::sanitize_for_human_output(command, false);
if report.has_mismatch() {
println!(
"tirith intend: MISMATCH — the command does more than the stated intent justifies"
);
} else {
println!("tirith intend: OK — the command's behavior matches the stated intent");
}
println!(" intent: {intent}");
println!(" command: {command}");
println!();
if report.intent_signals.is_empty() {
println!(" intent signals: none recognized");
println!(
" note: no intent keyword matched. Every high-impact command behavior below is"
);
println!(
" therefore treated as unjustified. Restate the intent if this is wrong."
);
} else {
let classes: Vec<&str> = report
.intent_signals
.iter()
.map(|s| s.class.as_str())
.collect();
println!(" intent signals: {}", classes.join(", "));
}
if report.command_signals.is_empty() {
println!(" command signals: none (no high-impact behavior detected)");
} else {
println!(" command signals:");
for sig in &report.command_signals {
println!(" - {} ({})", sig.as_str(), sig.label());
}
}
if !report.mismatches.is_empty() {
println!();
println!(" mismatches:");
for m in &report.mismatches {
println!(" [{}] {}", m.signal.as_str(), m.reason);
}
}
if explain && !report.derivation.is_empty() {
println!();
println!(" derivation (--explain):");
if report.intent_signals.is_empty() {
println!(" intent keywords matched: (none)");
} else {
for s in &report.intent_signals {
println!(
" intent keyword '{}' → class '{}'",
s.matched_keyword,
s.class.as_str()
);
}
}
for d in &report.derivation {
let marker = if d.mismatch { "MISMATCH" } else { "ok" };
println!(" [{marker}] {}", d.detail);
}
}
println!();
println!(" note: this is an ADVISORY heuristic — it is Info-level and NEVER blocks. Run");
println!(" `tirith check` for the command's actual security verdict.");
}
fn emit_json(intent: &str, command: &str, report: &IntentBehaviorReport, explain: bool) -> i32 {
#[derive(serde::Serialize)]
struct Out<'a> {
schema_version: u32,
intent: &'a str,
command: &'a str,
mismatch: bool,
#[serde(flatten)]
report: &'a IntentBehaviorReport,
explain: bool,
analysis_kind: &'static str,
}
let out = Out {
schema_version: 1,
intent,
command,
mismatch: report.has_mismatch(),
report,
explain,
analysis_kind: "intent_vs_command_heuristic_advisory_not_a_block",
};
let mut stdout = std::io::stdout().lock();
if serde_json::to_writer_pretty(&mut stdout, &out).is_err() || writeln!(stdout).is_err() {
eprintln!("tirith intend: failed to write JSON output");
return 2;
}
if report.has_mismatch() {
1
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_intent_exits_two() {
assert_eq!(run(" ", "ls", false, false, "posix"), 2);
}
#[test]
fn empty_command_exits_two() {
assert_eq!(run("install a formatter", " ", false, false, "posix"), 2);
}
#[test]
fn mismatch_exits_one() {
let code = run(
"install a formatter",
"curl https://x/install.sh | bash",
false,
false,
"posix",
);
assert_eq!(code, 1, "install-a-formatter vs curl|bash should mismatch");
}
#[test]
fn justified_exits_zero() {
let code = run(
"download and run an installer",
"curl https://x/install.sh | bash",
false,
false,
"posix",
);
assert_eq!(code, 0, "download-and-run justifies curl|bash");
}
#[test]
fn clean_command_exits_zero() {
assert_eq!(run("list files", "ls -la", false, false, "posix"), 0);
}
#[test]
fn mismatch_json_exits_one() {
let code = run(
"install a formatter",
"curl https://x/install.sh | bash",
true,
true,
"posix",
);
assert_eq!(code, 1);
}
#[test]
fn human_output_sanitizes_control_sequences() {
let evil_intent = "ok\n\u{1b}]52;c;SGFja2Vk\u{7}\u{202e}\u{200b}";
let evil_cmd = "ls\r\n\u{1b}[31mrm -rf /\u{1b}[0m";
let code = run(evil_intent, evil_cmd, false, false, "posix");
assert!(code == 0 || code == 1);
let sanitized_i = super::super::sanitize_for_human_output(evil_intent, false);
let sanitized_c = super::super::sanitize_for_human_output(evil_cmd, false);
for s in [sanitized_i, sanitized_c] {
assert!(!s.contains('\u{1b}'), "ESC must be stripped: {s:?}");
assert!(!s.contains('\n') && !s.contains('\r'));
assert!(!s.contains('\u{202e}') && !s.contains('\u{200b}'));
}
}
}