use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Parser, Debug)]
#[command(
name = "ilo",
version,
about = "Token-minimal programming language for AI agents"
)]
#[command(args_conflicts_with_subcommands = true)]
#[command(disable_help_subcommand = true)]
#[command(disable_help_flag = true)]
#[command(disable_version_flag = true)]
pub struct Cli {
#[command(subcommand)]
pub cmd: Option<Cmd>,
#[command(flatten)]
pub global: Global,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub args: Vec<String>,
}
#[derive(Args, Debug, Clone)]
pub struct Global {
#[arg(long, short = 'a', global = true)]
pub ansi: bool,
#[arg(long, short = 't', global = true, conflicts_with = "ansi")]
pub text: bool,
#[arg(long, short = 'j', global = true, conflicts_with_all = ["ansi", "text"])]
pub json: bool,
#[arg(long = "no-hints", short = 'n', global = true)]
pub no_hints: bool,
#[arg(long, short = 's', global = true)]
pub silent: bool,
#[arg(long = "max-ast-depth", global = true)]
pub max_ast_depth: Option<usize>,
#[arg(long = "max-runtime", global = true)]
pub max_runtime: Option<u64>,
#[arg(long = "max-output-bytes", global = true)]
pub max_output_bytes: Option<u64>,
}
#[derive(Subcommand, Debug)]
pub enum Cmd {
Run(RunArgs),
Repl,
Serv(ServArgs),
#[command(alias = "tool")]
Tools(ToolsArgs),
Graph(GraphArgs),
Compile(CompileArgs),
Build(CompileArgs),
Check(CheckArgs),
#[command(alias = "help")]
Spec(SpecArgs),
Explain(ExplainArgs),
Skill(SkillArgs),
Test(TestArgs),
Httpd(HttpdArgs),
Version,
Add(AddArgs),
Update(UpdateArgs),
Trace(TraceArgs),
}
#[derive(Args, Debug)]
pub struct RunArgs {
pub source: String,
#[arg(skip = Engine::Default)]
pub engine: Engine,
#[arg(skip = false)]
pub run_tree: bool,
#[arg(skip = false)]
pub run: bool,
#[arg(long = "vm", visible_alias = "run-vm", conflicts_with_all = ["jit", "run_llvm"])]
pub run_vm: bool,
#[arg(long = "jit", conflicts_with_all = ["run_vm", "run_llvm"])]
pub jit: bool,
#[arg(long = "run-llvm", conflicts_with_all = ["run_vm", "jit"])]
pub run_llvm: bool,
#[arg(long)]
pub bench: bool,
#[arg(long)]
pub emit: Option<String>,
#[arg(long = "explain", short = 'x')]
pub explain: bool,
#[arg(long, short = 'd', aliases = ["fmt"])]
pub dense: bool,
#[arg(long, short = 'e', aliases = ["fmt-expanded"])]
pub expanded: bool,
#[arg(long = "ast")]
pub ast: bool,
#[arg(long = "tools")]
pub tools_path: Option<String>,
#[arg(long = "mcp")]
pub mcp_path: Option<String>,
#[arg(long = "allow-net", value_name = "HOSTS")]
pub allow_net: Option<String>,
#[arg(long = "allow-read", value_name = "PATHS")]
pub allow_read: Option<String>,
#[arg(long = "allow-write", value_name = "PATHS")]
pub allow_write: Option<String>,
#[arg(long = "allow-run", value_name = "CMDS")]
pub allow_run: Option<String>,
#[arg(long = "allow-env", value_name = "VARS")]
pub allow_env: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub rest: Vec<String>,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Engine {
Default,
Tree,
Vm,
Cranelift,
Llvm,
}
impl RunArgs {
pub fn effective_engine(&self) -> Engine {
if self.run || self.run_tree {
Engine::Tree
} else if self.run_vm {
Engine::Vm
} else if self.jit {
Engine::Cranelift
} else if self.run_llvm {
Engine::Llvm
} else {
self.engine
}
}
}
#[derive(Args, Debug)]
pub struct ServArgs {
#[arg(long = "mcp", short = 'm')]
pub mcp_path: Option<String>,
#[arg(long = "tools")]
pub tools_path: Option<String>,
}
#[derive(Args, Debug)]
pub struct HttpdArgs {
#[arg(long, short = 'p', default_value = "8080")]
pub port: u16,
pub handler: String,
pub func: Option<String>,
}
#[derive(Args, Debug)]
pub struct ToolsArgs {
#[arg(long = "mcp", short = 'm')]
pub mcp_path: Option<String>,
#[arg(long = "tools")]
pub tools_path: Option<String>,
#[arg(long, value_enum)]
pub format: Option<ToolsFormat>,
#[arg(long)]
pub human: bool,
#[arg(long)]
pub ilo: bool,
#[arg(long, short = 'j')]
pub json: bool,
#[arg(long, short = 'f')]
pub full: bool,
#[arg(long, short = 'g')]
pub graph: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolsFormat {
Human,
Ilo,
Json,
}
#[derive(Args, Debug)]
pub struct GraphArgs {
pub file: String,
#[arg(long = "fn")]
pub fn_name: Option<String>,
#[arg(long)]
pub reverse: bool,
#[arg(long)]
pub subgraph: bool,
#[arg(long)]
pub budget: Option<usize>,
#[arg(long)]
pub dot: bool,
}
pub const SUPPORTED_TARGETS: &[&str] = &[
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-musl",
"x86_64-pc-windows-msvc",
"wasm32-wasip1",
];
#[derive(Args, Debug)]
pub struct CompileArgs {
pub source: String,
#[arg(short = 'o')]
pub output: Option<String>,
pub func: Option<String>,
#[arg(long)]
pub bench: bool,
#[arg(long, value_name = "TRIPLE")]
pub target: Option<String>,
}
#[derive(Args, Debug)]
pub struct CheckArgs {
pub source: String,
#[arg(long)]
pub strict: bool,
#[arg(long)]
pub show_effects: bool,
}
#[derive(Args, Debug)]
pub struct TestArgs {
pub path: Option<String>,
#[arg(long, value_enum, default_value = "vm")]
pub engine: TestEngine,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum TestEngine {
Vm,
Jit,
All,
}
#[derive(Args, Debug)]
pub struct SpecArgs {
pub topic: Option<String>,
}
#[derive(Args, Debug)]
pub struct ExplainArgs {
pub code: String,
}
#[derive(Args, Debug)]
pub struct SkillArgs {
#[command(subcommand)]
pub cmd: SkillCmd,
}
#[derive(Subcommand, Debug)]
pub enum SkillCmd {
List,
Get { name: String },
Path { name: String },
Show { name: String },
}
#[derive(Args, Debug)]
pub struct AddArgs {
pub package: String,
}
#[derive(Args, Debug)]
pub struct UpdateArgs {
pub package: Option<String>,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TraceDepth {
#[default]
Statement,
Expr,
}
#[derive(Args, Debug, Clone)]
pub struct TraceArgs {
#[arg(value_name = "FILE")]
pub source: String,
#[arg(value_name = "FUNC")]
pub func: Option<String>,
#[arg(long = "depth", value_enum, default_value = "statement")]
pub depth: TraceDepth,
#[arg(long = "watch", value_name = "NAME", action = clap::ArgAction::Append)]
pub watch: Vec<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub rest: Vec<String>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum OutputMode {
Ansi,
Text,
Json,
}
impl Global {
pub fn output_mode(&self) -> OutputMode {
if self.ansi {
return OutputMode::Ansi;
}
if self.text {
return OutputMode::Text;
}
if self.json {
return OutputMode::Json;
}
use std::io::IsTerminal;
let is_tty = std::io::stderr().is_terminal();
let no_color = std::env::var("NO_COLOR").is_ok();
if is_tty && !no_color {
OutputMode::Ansi
} else if is_tty {
OutputMode::Text
} else {
OutputMode::Json
}
}
pub fn explicit_json(&self) -> bool {
self.json
}
}
pub fn reject_unknown_flags(args: &[String]) -> Result<(), String> {
reject_unknown_flags_with_allowlist(args, &[])
}
pub fn reject_unknown_flags_with_allowlist(
args: &[String],
allowlist: &[&str],
) -> Result<(), String> {
for a in args {
if a == "--" {
return Ok(());
}
let head = a.split_once('=').map(|(h, _)| h).unwrap_or(a.as_str());
if looks_like_clean_long_flag(head)
&& !allowlist.contains(&head)
&& !allowlist.contains(&a.as_str())
{
return Err(format!(
"error: unrecognised flag '{a}'. Use 'ilo --help' for valid flags. To pass it as a literal arg, separate with '--' first."
));
}
}
Ok(())
}
fn looks_like_clean_long_flag(s: &str) -> bool {
let Some(rest) = s.strip_prefix("--") else {
return false;
};
if rest.is_empty() {
return false; }
let bytes = rest.as_bytes();
if !bytes[0].is_ascii_lowercase() {
return false;
}
let mut prev_dash = false;
for (i, &b) in bytes.iter().enumerate() {
if b == b'-' {
if prev_dash || i + 1 == bytes.len() {
return false;
}
prev_dash = true;
} else if b.is_ascii_lowercase() || b.is_ascii_digit() {
prev_dash = false;
} else {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_run_subcommand() {
let cli = Cli::try_parse_from(["ilo", "run", "file.ilo", "func", "42"]).unwrap();
match cli.cmd {
Some(Cmd::Run(r)) => {
assert_eq!(r.source, "file.ilo");
assert_eq!(r.rest, vec!["func", "42"]);
}
other => panic!("expected Run, got {other:?}"),
}
}
#[test]
fn parse_repl_subcommand() {
let cli = Cli::try_parse_from(["ilo", "repl"]).unwrap();
assert!(matches!(cli.cmd, Some(Cmd::Repl)));
}
#[test]
fn parse_serv_with_mcp() {
let cli = Cli::try_parse_from(["ilo", "serv", "--mcp", "cfg.json"]).unwrap();
match cli.cmd {
Some(Cmd::Serv(s)) => assert_eq!(s.mcp_path.as_deref(), Some("cfg.json")),
other => panic!("expected Serv, got {other:?}"),
}
}
#[test]
fn parse_tools_with_flags() {
let cli =
Cli::try_parse_from(["ilo", "tools", "--mcp", "p.json", "--full", "--graph"]).unwrap();
match cli.cmd {
Some(Cmd::Tools(t)) => {
assert_eq!(t.mcp_path.as_deref(), Some("p.json"));
assert!(t.full);
assert!(t.graph);
}
other => panic!("expected Tools, got {other:?}"),
}
}
#[test]
fn parse_graph_subcommand() {
let cli =
Cli::try_parse_from(["ilo", "graph", "file.ilo", "--fn", "main", "--dot"]).unwrap();
match cli.cmd {
Some(Cmd::Graph(g)) => {
assert_eq!(g.file, "file.ilo");
assert_eq!(g.fn_name.as_deref(), Some("main"));
assert!(g.dot);
}
other => panic!("expected Graph, got {other:?}"),
}
}
#[test]
fn parse_compile_subcommand() {
let cli =
Cli::try_parse_from(["ilo", "compile", "prog.ilo", "-o", "out", "--bench"]).unwrap();
match cli.cmd {
Some(Cmd::Compile(c)) => {
assert_eq!(c.source, "prog.ilo");
assert_eq!(c.output.as_deref(), Some("out"));
assert!(c.bench);
}
other => panic!("expected Compile, got {other:?}"),
}
}
#[test]
fn parse_global_json_flag() {
let cli = Cli::try_parse_from(["ilo", "--json", "repl"]).unwrap();
assert!(cli.global.json);
assert_eq!(cli.global.output_mode(), OutputMode::Json);
}
#[test]
fn parse_global_ansi_flag() {
let cli = Cli::try_parse_from(["ilo", "-a", "repl"]).unwrap();
assert!(cli.global.ansi);
assert_eq!(cli.global.output_mode(), OutputMode::Ansi);
}
#[test]
fn parse_global_text_flag() {
let cli = Cli::try_parse_from(["ilo", "--text", "repl"]).unwrap();
assert!(cli.global.text);
assert_eq!(cli.global.output_mode(), OutputMode::Text);
}
#[test]
fn parse_global_no_hints() {
let cli = Cli::try_parse_from(["ilo", "-n", "repl"]).unwrap();
assert!(cli.global.no_hints);
}
#[test]
fn parse_explain_subcommand() {
let cli = Cli::try_parse_from(["ilo", "explain", "ILO-T005"]).unwrap();
match cli.cmd {
Some(Cmd::Explain(e)) => assert_eq!(e.code, "ILO-T005"),
other => panic!("expected Explain, got {other:?}"),
}
}
#[test]
fn parse_version_subcommand() {
let cli = Cli::try_parse_from(["ilo", "version"]).unwrap();
assert!(matches!(cli.cmd, Some(Cmd::Version)));
}
#[test]
fn parse_tool_alias() {
let cli = Cli::try_parse_from(["ilo", "tool", "--mcp", "p.json"]).unwrap();
assert!(matches!(cli.cmd, Some(Cmd::Tools(_))));
}
#[test]
fn parse_spec_subcommand_lang() {
let cli = Cli::try_parse_from(["ilo", "spec", "lang"]).unwrap();
match cli.cmd {
Some(Cmd::Spec(s)) => assert_eq!(s.topic.as_deref(), Some("lang")),
other => panic!("expected Spec, got {other:?}"),
}
}
#[test]
fn parse_spec_subcommand_ai() {
let cli = Cli::try_parse_from(["ilo", "spec", "ai"]).unwrap();
match cli.cmd {
Some(Cmd::Spec(s)) => assert_eq!(s.topic.as_deref(), Some("ai")),
other => panic!("expected Spec, got {other:?}"),
}
}
#[test]
fn run_tree_flag_rejected_by_clap() {
let err = Cli::try_parse_from(["ilo", "run", "--run-tree", "code"]).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("--run-tree") || msg.contains("unexpected"),
"expected clap to reject --run-tree; got: {msg}"
);
}
#[test]
fn engine_flag_vm_canonical() {
let cli = Cli::try_parse_from(["ilo", "run", "--vm", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert!(r.run_vm);
assert_eq!(r.effective_engine(), Engine::Vm);
} else {
panic!("expected Run subcommand");
}
}
#[test]
fn engine_flag_run_vm_alias_still_parses() {
let cli = Cli::try_parse_from(["ilo", "run", "--run-vm", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert!(r.run_vm);
assert_eq!(r.effective_engine(), Engine::Vm);
} else {
panic!("expected Run subcommand");
}
}
#[test]
fn engine_flag_vm_conflicts_with_jit() {
let err = Cli::try_parse_from(["ilo", "run", "--vm", "--jit", "code"]).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("cannot be used with") || msg.contains("conflict"),
"expected clap conflict error; got: {msg}"
);
}
#[test]
fn default_positional_args_fallback() {
let cli = Cli::try_parse_from(["ilo", "f>n;42", "5"]).unwrap();
assert!(cli.cmd.is_none());
assert_eq!(cli.args, vec!["f>n;42", "5"]);
}
#[test]
fn tools_json_shorthand() {
let cli = Cli::try_parse_from(["ilo", "tools", "--mcp", "p.json", "--json"]).unwrap();
if let Some(Cmd::Tools(t)) = cli.cmd {
assert!(t.json);
}
}
#[test]
fn tools_ilo_shorthand() {
let cli = Cli::try_parse_from(["ilo", "tools", "--mcp", "p.json", "--ilo"]).unwrap();
if let Some(Cmd::Tools(t)) = cli.cmd {
assert!(t.ilo);
}
}
#[test]
fn tools_human_shorthand() {
let cli = Cli::try_parse_from(["ilo", "tools", "--mcp", "p.json", "--human"]).unwrap();
if let Some(Cmd::Tools(t)) = cli.cmd {
assert!(t.human);
}
}
#[test]
fn compile_with_func() {
let cli = Cli::try_parse_from(["ilo", "compile", "prog.ilo", "entry"]).unwrap();
if let Some(Cmd::Compile(c)) = cli.cmd {
assert_eq!(c.func.as_deref(), Some("entry"));
}
}
#[test]
fn compile_with_target() {
let cli = Cli::try_parse_from(["ilo", "compile", "prog.ilo", "--target", "wasm32-wasip1"])
.unwrap();
if let Some(Cmd::Compile(c)) = cli.cmd {
assert_eq!(c.target.as_deref(), Some("wasm32-wasip1"));
}
}
#[test]
fn compile_target_flag_parses_all_supported() {
for triple in SUPPORTED_TARGETS {
let cli =
Cli::try_parse_from(["ilo", "compile", "prog.ilo", "--target", triple]).unwrap();
if let Some(Cmd::Compile(c)) = cli.cmd {
assert_eq!(c.target.as_deref(), Some(*triple));
} else {
panic!("expected Compile for target {triple}");
}
}
}
#[test]
fn graph_with_budget() {
let cli = Cli::try_parse_from(["ilo", "graph", "f.ilo", "--budget", "100"]).unwrap();
if let Some(Cmd::Graph(g)) = cli.cmd {
assert_eq!(g.budget, Some(100));
}
}
#[test]
fn graph_with_reverse() {
let cli = Cli::try_parse_from(["ilo", "graph", "f.ilo", "--reverse"]).unwrap();
if let Some(Cmd::Graph(g)) = cli.cmd {
assert!(g.reverse);
}
}
#[test]
fn graph_with_subgraph() {
let cli = Cli::try_parse_from(["ilo", "graph", "f.ilo", "--subgraph"]).unwrap();
if let Some(Cmd::Graph(g)) = cli.cmd {
assert!(g.subgraph);
}
}
#[test]
fn run_with_bench() {
let cli = Cli::try_parse_from(["ilo", "run", "--bench", "code", "func", "42"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert!(r.bench);
assert_eq!(r.source, "code");
}
}
#[test]
fn run_with_emit_python() {
let cli = Cli::try_parse_from(["ilo", "run", "--emit", "python", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert_eq!(r.emit.as_deref(), Some("python"));
}
}
#[test]
fn run_with_explain() {
let cli = Cli::try_parse_from(["ilo", "run", "--explain", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert!(r.explain);
}
}
#[test]
fn run_with_dense() {
let cli = Cli::try_parse_from(["ilo", "run", "--dense", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert!(r.dense);
}
}
#[test]
fn run_with_expanded() {
let cli = Cli::try_parse_from(["ilo", "run", "--expanded", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert!(r.expanded);
}
}
#[test]
fn serv_with_tools() {
let cli = Cli::try_parse_from(["ilo", "serv", "--tools", "http.json"]).unwrap();
if let Some(Cmd::Serv(s)) = cli.cmd {
assert_eq!(s.tools_path.as_deref(), Some("http.json"));
}
}
#[test]
fn run_with_tools_and_mcp() {
let cli = Cli::try_parse_from(["ilo", "run", "--tools", "http.json", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert_eq!(r.tools_path.as_deref(), Some("http.json"));
}
}
#[test]
fn help_alias_for_spec() {
let cli = Cli::try_parse_from(["ilo", "help", "ai"]).unwrap();
assert!(matches!(cli.cmd, Some(Cmd::Spec(_))));
}
#[test]
fn engine_flag_jit() {
let cli = Cli::try_parse_from(["ilo", "run", "--jit", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert_eq!(r.effective_engine(), Engine::Cranelift);
} else {
panic!("expected Run subcommand");
}
}
#[test]
fn engine_flag_run_llvm() {
let cli = Cli::try_parse_from(["ilo", "run", "--run-llvm", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert_eq!(r.effective_engine(), Engine::Llvm);
} else {
panic!("expected Run subcommand");
}
}
#[test]
fn run_alias_flag_rejected_by_clap() {
let err = Cli::try_parse_from(["ilo", "run", "--run", "code"]).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("--run") || msg.contains("unexpected"),
"expected clap to reject --run as a flag; got: {msg}"
);
}
#[test]
fn engine_default_when_no_flags() {
let r = RunArgs {
source: "code".to_string(),
engine: Engine::Default,
run_tree: false,
run: false,
run_vm: false,
jit: false,
run_llvm: false,
bench: false,
emit: None,
explain: false,
dense: false,
expanded: false,
ast: false,
tools_path: None,
mcp_path: None,
allow_net: None,
allow_read: None,
allow_write: None,
allow_run: None,
allow_env: None,
rest: vec![],
};
assert_eq!(r.effective_engine(), Engine::Default);
}
#[test]
fn output_mode_no_color_env_returns_text_when_tty_unavailable() {
let g = Global {
ansi: false,
text: false,
json: false,
no_hints: false,
silent: false,
max_ast_depth: None,
max_runtime: None,
max_output_bytes: None,
};
assert!(!g.explicit_json());
let mode = g.output_mode();
assert!(
matches!(mode, OutputMode::Ansi | OutputMode::Text | OutputMode::Json),
"output_mode should return a valid mode"
);
}
#[test]
fn global_explicit_json_true_when_json_flag_set() {
let g = Global {
ansi: false,
text: false,
json: true,
no_hints: false,
silent: false,
max_ast_depth: None,
max_runtime: None,
max_output_bytes: None,
};
assert!(g.explicit_json());
assert_eq!(g.output_mode(), OutputMode::Json);
}
#[test]
fn global_explicit_json_false_when_text_set() {
let g = Global {
ansi: false,
text: true,
json: false,
no_hints: false,
silent: false,
max_ast_depth: None,
max_runtime: None,
max_output_bytes: None,
};
assert!(!g.explicit_json());
assert_eq!(g.output_mode(), OutputMode::Text);
}
#[test]
fn global_explicit_json_false_when_ansi_set() {
let g = Global {
ansi: true,
text: false,
json: false,
no_hints: false,
silent: false,
max_ast_depth: None,
max_runtime: None,
max_output_bytes: None,
};
assert!(!g.explicit_json());
assert_eq!(g.output_mode(), OutputMode::Ansi);
}
#[test]
fn tools_format_human_parse() {
let cli =
Cli::try_parse_from(["ilo", "tools", "--mcp", "p.json", "--format", "human"]).unwrap();
if let Some(Cmd::Tools(t)) = cli.cmd {
assert_eq!(t.format, Some(ToolsFormat::Human));
}
}
#[test]
fn tools_format_ilo_parse() {
let cli =
Cli::try_parse_from(["ilo", "tools", "--mcp", "p.json", "--format", "ilo"]).unwrap();
if let Some(Cmd::Tools(t)) = cli.cmd {
assert_eq!(t.format, Some(ToolsFormat::Ilo));
}
}
#[test]
fn tools_format_json_parse() {
let cli =
Cli::try_parse_from(["ilo", "tools", "--mcp", "p.json", "--format", "json"]).unwrap();
if let Some(Cmd::Tools(t)) = cli.cmd {
assert_eq!(t.format, Some(ToolsFormat::Json));
}
}
#[test]
fn graph_with_fn_name() {
let cli = Cli::try_parse_from(["ilo", "graph", "f.ilo", "--fn", "main"]).unwrap();
if let Some(Cmd::Graph(g)) = cli.cmd {
assert_eq!(g.fn_name.as_deref(), Some("main"));
}
}
#[test]
fn unknown_long_flag_rejected() {
let args = vec![
"main.ilo".to_string(),
"--engine".to_string(),
"tree".to_string(),
];
let err = reject_unknown_flags(&args).unwrap_err();
assert!(err.contains("--engine"), "msg={err}");
assert!(err.contains("unrecognised flag"));
assert!(err.contains("'--' first"));
}
#[test]
fn unknown_long_flag_no_value_rejected() {
let args = vec!["main.ilo".to_string(), "--foo".to_string()];
assert!(reject_unknown_flags(&args).is_err());
}
#[test]
fn unknown_hyphenated_flag_rejected() {
let args = vec!["main.ilo".to_string(), "--some-long-flag".to_string()];
assert!(reject_unknown_flags(&args).is_err());
}
#[test]
fn dash_dash_separator_escapes_subsequent_flags() {
let args = vec![
"main.ilo".to_string(),
"--".to_string(),
"--foo".to_string(),
"--engine".to_string(),
];
assert!(reject_unknown_flags(&args).is_ok());
}
#[test]
fn plain_positional_args_accepted() {
let args = vec!["main.ilo".to_string(), "func".to_string(), "42".to_string()];
assert!(reject_unknown_flags(&args).is_ok());
}
#[test]
fn negative_number_not_treated_as_flag() {
let args = vec![
"main.ilo".to_string(),
"-1".to_string(),
"-3.14".to_string(),
];
assert!(reject_unknown_flags(&args).is_ok());
}
#[test]
fn equals_form_unknown_flag_rejected() {
let args = vec!["main.ilo".to_string(), "--foo=bar".to_string()];
let err = reject_unknown_flags(&args).unwrap_err();
assert!(err.contains("--foo=bar"), "msg={err}");
assert!(err.contains("unrecognised flag"));
}
#[test]
fn equals_form_engine_rejected() {
let args = vec!["main.ilo".to_string(), "--engine=tree".to_string()];
let err = reject_unknown_flags(&args).unwrap_err();
assert!(err.contains("--engine=tree"), "msg={err}");
}
#[test]
fn equals_form_allowlisted_head_accepted() {
let args = vec!["main.ilo".to_string(), "--bench=on".to_string()];
assert!(reject_unknown_flags_with_allowlist(&args, &["--bench"]).is_ok());
}
#[test]
fn equals_form_after_dash_dash_accepted() {
let args = vec![
"main.ilo".to_string(),
"--".to_string(),
"--foo=bar".to_string(),
];
assert!(reject_unknown_flags(&args).is_ok());
}
#[test]
fn equals_form_with_empty_value_rejected() {
let args = vec!["main.ilo".to_string(), "--foo=".to_string()];
assert!(reject_unknown_flags(&args).is_err());
}
#[test]
fn equals_form_with_non_flag_head_accepted() {
let args = vec!["main.ilo".to_string(), "key=value".to_string()];
assert!(reject_unknown_flags(&args).is_ok());
}
#[test]
fn trailing_dash_not_treated_as_flag() {
let args = vec!["main.ilo".to_string(), "--foo-".to_string()];
assert!(reject_unknown_flags(&args).is_ok());
}
#[test]
fn doubled_dash_inside_not_treated_as_flag() {
let args = vec!["main.ilo".to_string(), "--foo--bar".to_string()];
assert!(reject_unknown_flags(&args).is_ok());
}
#[test]
fn empty_args_ok() {
let args: Vec<String> = vec![];
assert!(reject_unknown_flags(&args).is_ok());
}
#[test]
fn looks_like_clean_long_flag_shapes() {
assert!(looks_like_clean_long_flag("--foo"));
assert!(looks_like_clean_long_flag("--engine"));
assert!(looks_like_clean_long_flag("--some-long-flag"));
assert!(looks_like_clean_long_flag("--a1"));
assert!(!looks_like_clean_long_flag("--"));
assert!(!looks_like_clean_long_flag("-x"));
assert!(!looks_like_clean_long_flag("--Foo"));
assert!(!looks_like_clean_long_flag("--foo=bar"));
assert!(!looks_like_clean_long_flag("--foo-"));
assert!(!looks_like_clean_long_flag("--foo--bar"));
assert!(!looks_like_clean_long_flag("--1foo"));
assert!(!looks_like_clean_long_flag("foo"));
assert!(!looks_like_clean_long_flag("-1"));
}
#[test]
fn run_with_mcp_path() {
let cli = Cli::try_parse_from(["ilo", "run", "--mcp", "cfg.json", "code"]).unwrap();
if let Some(Cmd::Run(r)) = cli.cmd {
assert_eq!(r.mcp_path.as_deref(), Some("cfg.json"));
}
}
}