1use std::path::PathBuf;
26
27use rpi_ai::ThinkingLevel;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum Mode {
34 #[default]
35 Text,
36 Json,
37 Rpc,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum TuiMode {
44 #[default]
45 Fullscreen,
46 Regular,
47}
48
49#[derive(Debug, Clone, Default)]
53pub struct Args {
54 pub provider: Option<String>,
55 pub model: Option<String>,
56 pub api_key: Option<String>,
57 pub base_url: Option<String>,
60 pub system_prompt: Option<String>,
61 pub append_system_prompt: Vec<String>,
62 pub theme: Option<String>,
64 pub thinking: Option<ThinkingLevel>,
65
66 pub print: bool,
67 pub mode: Mode,
68 pub tui_mode: TuiMode,
71
72 pub list_models: Option<String>,
75 pub offline: bool,
77 pub export: Option<PathBuf>,
80 pub trust_override: Option<bool>,
83
84 pub continue_session: bool,
85 pub resume: bool,
86 pub session: Option<String>,
87 pub session_id: Option<String>,
91 pub fork: Option<String>,
94 pub models: Option<Vec<String>>,
98 pub session_dir: Option<PathBuf>,
99 pub no_session: bool,
100 pub name: Option<String>,
101
102 pub tools: Option<Vec<String>>,
103 pub exclude_tools: Option<Vec<String>>,
104 pub no_tools: bool,
105 pub no_builtin_tools: bool,
106
107 pub no_skills: bool,
110 pub no_prompt_templates: bool,
113 pub no_context_files: bool,
116 pub no_extensions: bool,
121 pub enable_pi_packages: bool,
125 pub no_themes: bool,
128 pub extensions_dir: Vec<PathBuf>,
136 pub extension: Vec<PathBuf>,
139 pub skill: Vec<PathBuf>,
141 pub prompt_template: Vec<PathBuf>,
144
145 pub verbose: bool,
146 pub help: bool,
147 pub version: bool,
148
149 pub debug_system_prompt: bool,
156
157 pub messages: Vec<String>,
159 pub file_args: Vec<PathBuf>,
162
163 pub ignored: Vec<String>,
166 pub errors: Vec<String>,
169}
170
171pub const VALID_THINKING_LEVELS: &[&str] =
174 &["off", "minimal", "low", "medium", "high", "xhigh", "max"];
175
176pub fn parse_thinking_level(s: &str) -> Option<ThinkingLevel> {
178 Some(match s {
179 "off" => ThinkingLevel::Off,
180 "minimal" => ThinkingLevel::Minimal,
181 "low" => ThinkingLevel::Low,
182 "medium" => ThinkingLevel::Medium,
183 "high" => ThinkingLevel::High,
184 "xhigh" => ThinkingLevel::Xhigh,
185 "max" => ThinkingLevel::Max,
186 _ => return None,
187 })
188}
189
190fn file_arg(arg: &str) -> Option<PathBuf> {
193 if let Some(rest) = arg.strip_prefix('@') {
194 if rest.is_empty() {
196 None
197 } else {
198 Some(PathBuf::from(rest))
199 }
200 } else {
201 None
202 }
203}
204
205pub fn parse_args(args: &[String]) -> Args {
211 let mut result = Args::default();
212 if let Ok(raw) = std::env::var("RPI_EXTENSIONS_DIR") {
216 if !raw.is_empty() {
217 let sep = if cfg!(windows) { ';' } else { ':' };
218 for part in raw.split(sep) {
219 let trimmed = part.trim();
220 if !trimmed.is_empty() {
221 result.extensions_dir.push(PathBuf::from(trimmed));
222 }
223 }
224 }
225 }
226 let mut i = 0;
227 while i < args.len() {
228 let arg = args[i].clone();
229 let (flag_key, inline) = if arg.starts_with("--") {
233 match arg.find('=') {
234 Some(eq) => (arg[..eq].to_string(), Some(arg[eq + 1..].to_string())),
235 None => (arg.clone(), None),
236 }
237 } else {
238 (arg.clone(), None)
239 };
240
241 let mut take_value = |result: &mut Args, _flag: &str| -> Option<String> {
245 if let Some(v) = inline.clone() {
246 return Some(v);
247 }
248 if i + 1 < args.len() {
249 let next = &args[i + 1];
250 if !next.starts_with('-') || next == "-" {
251 i += 1;
252 return Some(args[i].clone());
253 }
254 }
255 result.errors.push(format!("{flag_key} requires a value"));
256 None
257 };
258
259 match flag_key.as_str() {
260 "--help" | "-h" => result.help = true,
261 "--version" | "-v" => result.version = true,
262 "--print" | "-p" => {
263 result.print = true;
264 if i + 1 < args.len() {
269 let next = &args[i + 1];
270 if !next.starts_with('@') && !next.starts_with('-') {
271 i += 1;
272 result.messages.push(args[i].clone());
273 }
274 }
275 }
276 "--mode" => {
277 if let Some(v) = take_value(&mut result, "--mode") {
278 result.mode = match v.as_str() {
279 "text" => Mode::Text,
280 "json" => Mode::Json,
281 "rpc" => Mode::Rpc,
282 other => {
283 result.errors.push(format!(
284 "Invalid --mode \"{other}\". Valid: text, json, rpc"
285 ));
286 Mode::Text
287 }
288 };
289 }
290 }
291 "--tui-mode" => {
292 if let Some(v) = take_value(&mut result, "--tui-mode") {
293 result.tui_mode = match v.to_ascii_lowercase().as_str() {
294 "regular" => TuiMode::Regular,
295 "fullscreen" => TuiMode::Fullscreen,
296 other => {
297 result.errors.push(format!(
298 "Invalid --tui-mode \"{other}\". Valid: regular, fullscreen"
299 ));
300 TuiMode::Fullscreen
301 }
302 };
303 }
304 }
305 "--continue" | "-c" => result.continue_session = true,
306 "--resume" | "-r" => result.resume = true,
307 "--no-session" => result.no_session = true,
308 "--no-tools" | "-nt" => result.no_tools = true,
309 "--no-builtin-tools" | "-nbt" => result.no_builtin_tools = true,
310 "--no-skills" | "-ns" => result.no_skills = true,
311 "--no-prompt-templates" | "-np" => result.no_prompt_templates = true,
312 "--no-context-files" | "-nc" => result.no_context_files = true,
313 "--no-extensions" | "-ne" => result.no_extensions = true,
314 "--enable-pi-packages" => result.enable_pi_packages = true,
315 "--extensions-dir" | "-ed" => {
316 if let Some(v) = take_value(&mut result, &flag_key) {
317 result.extensions_dir.push(PathBuf::from(v));
318 }
319 }
320 "--verbose" => result.verbose = true,
321 "--debug-system-prompt" => result.debug_system_prompt = true,
322 "--provider" => result.provider = take_value(&mut result, "--provider"),
323 "--model" => result.model = take_value(&mut result, "--model"),
324 "--api-key" => result.api_key = take_value(&mut result, "--api-key"),
325 "--base-url" => result.base_url = take_value(&mut result, "--base-url"),
326 "--system-prompt" => result.system_prompt = take_value(&mut result, "--system-prompt"),
327 "--append-system-prompt" => {
328 if let Some(v) = take_value(&mut result, "--append-system-prompt") {
329 result.append_system_prompt.push(v);
330 }
331 }
332 "--name" | "-n" => result.name = take_value(&mut result, "--name"),
333 "--session" => result.session = take_value(&mut result, "--session"),
334 "--session-id" => result.session_id = take_value(&mut result, "--session-id"),
335 "--fork" => result.fork = take_value(&mut result, "--fork"),
336 "--models" => {
337 if let Some(v) = take_value(&mut result, &flag_key) {
338 result.models = Some(split_csv(&v));
339 }
340 }
341 "--extension" | "-e" => {
342 if let Some(v) = take_value(&mut result, &flag_key) {
343 result.extension.push(PathBuf::from(v));
344 }
345 }
346 "--skill" => {
347 if let Some(v) = take_value(&mut result, &flag_key) {
348 result.skill.push(PathBuf::from(v));
349 }
350 }
351 "--prompt-template" => {
352 if let Some(v) = take_value(&mut result, &flag_key) {
353 result.prompt_template.push(PathBuf::from(v));
354 }
355 }
356 "--session-dir" => {
357 if let Some(v) = take_value(&mut result, "--session-dir") {
358 result.session_dir = Some(PathBuf::from(v));
359 }
360 }
361 "--thinking" => {
362 if let Some(v) = take_value(&mut result, "--thinking") {
363 match parse_thinking_level(&v) {
364 Some(lvl) => result.thinking = Some(lvl),
365 None => result.ignored.push(format!(
366 "Invalid --thinking \"{v}\". Valid: {}",
367 VALID_THINKING_LEVELS.join(", ")
368 )),
369 }
370 }
371 }
372 "--tools" | "-t" => {
373 if let Some(v) = take_value(&mut result, &flag_key) {
374 result.tools = Some(split_csv(&v));
375 }
376 }
377 "--exclude-tools" | "-xt" => {
378 if let Some(v) = take_value(&mut result, &flag_key) {
379 result.exclude_tools = Some(split_csv(&v));
380 }
381 }
382 "--list-models" => {
383 let mut search = inline.clone().unwrap_or_default();
386 if inline.is_none()
387 && i + 1 < args.len()
388 && !args[i + 1].starts_with('-')
389 && !args[i + 1].starts_with('@')
390 {
391 i += 1;
392 search = args[i].clone();
393 }
394 result.list_models = Some(search);
395 }
396 "--offline" => result.offline = true,
397 "--export" => {
398 if let Some(value) = take_value(&mut result, &flag_key) {
399 result.export = Some(PathBuf::from(value));
400 }
401 }
402 "--approve" | "-a" => result.trust_override = Some(true),
403 "--no-approve" | "-na" => result.trust_override = Some(false),
404 other if matches!(other, "--models") => {
415 if inline.is_none()
418 && i + 1 < args.len()
419 && !args[i + 1].starts_with('-')
420 && !args[i + 1].starts_with('@')
421 {
422 i += 1;
423 }
424 result
425 .ignored
426 .push(format!("{other} is not supported in v1 (ignored)"));
427 }
428 "--theme" => {
429 result.theme = take_value(&mut result, "--theme");
430 }
431 "--no-themes" => result.no_themes = true,
432 other if other.starts_with("--") => {
436 let name = &flag_key;
437 if inline.is_none()
438 && i + 1 < args.len()
439 && !args[i + 1].starts_with('-')
440 && !args[i + 1].starts_with('@')
441 {
442 i += 1;
443 }
444 result
445 .ignored
446 .push(format!("{name} is not a recognized flag (ignored)"));
447 }
448 other if other.starts_with('-') && other.len() > 1 => {
450 result.errors.push(format!("Unknown option: {other}"));
451 }
452 other => {
453 if let Some(path) = file_arg(other) {
454 result.file_args.push(path);
455 } else {
456 result.messages.push(other.to_string());
457 }
458 }
459 }
460 i += 1;
461 }
462
463 result
467}
468
469fn split_csv(v: &str) -> Vec<String> {
471 v.split(',')
472 .map(|s| s.trim().to_string())
473 .filter(|s| !s.is_empty())
474 .collect()
475}
476
477pub fn resolve_mode(parsed: &Args, stdin_is_tty: bool, stdout_is_tty: bool) -> RunMode {
482 if parsed.mode == Mode::Rpc {
483 return RunMode::Rpc;
484 }
485 if parsed.mode == Mode::Json {
486 return RunMode::Json;
487 }
488 if parsed.print || !stdin_is_tty || !stdout_is_tty {
489 RunMode::Print
490 } else {
491 RunMode::Interactive
492 }
493}
494
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500pub enum RunMode {
501 Interactive,
502 Print,
503 Json,
504 Rpc,
505}
506
507pub fn print_help() {
509 let builtin = "read, bash, edit, write";
510 println!(
511 "{name} - AI coding assistant with read, bash, edit, write tools
512
513{u}Usage:{r}
514 {name} [options] [@files...] [messages...]
515
516{u}Options:{r}
517 --provider <name> Provider name (anthropic, openai-completions, openai-responses, or models.json id)
518 --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")
519 --api-key <key> API key override for the selected provider
520 --base-url <url> Override the selected model endpoint
521 --system-prompt <text> Replace the default system prompt
522 --append-system-prompt <text> Append text to the system prompt (repeatable)
523 --thinking <level> off, minimal, low, medium, high, xhigh, max
524 --mode <mode> Output mode: text (default), json, or rpc
525 --tui-mode <mode> Interactive TUI buffer: regular or fullscreen
526 --list-models [search] List available models (with optional fuzzy search)
527 --offline Disable startup network checks
528 --export <file> Export a JSONL session to HTML and exit
529 --approve, -a Trust the current project for local resources
530 --no-approve, -na Do not trust the current project
531 --print, -p Non-interactive: process prompt(s) and exit
532 --continue, -c Continue the most recent session
533 --resume, -r Browse and select a session to resume
534 --session <id|path> Use a specific session (partial UUID or file)
535 --session-dir <dir> Directory for session storage
536 --no-session Ephemeral mode (do not persist the session)
537 --name, -n <name> Set the session display name
538 --tools, -t <list> Comma-separated allowlist of tool names to enable
539 --exclude-tools, -xt <list> Comma-separated denylist of tool names to disable
540 --no-tools, -nt Disable all tools
541 --no-builtin-tools, -nbt Disable the built-in tools (read, bash, edit, write)
542 --no-skills, -ns Skip skill discovery (no <available_skills> block)
543 --no-prompt-templates, -np Skip prompt-template discovery (/expand templates)
544 --no-context-files, -nc Skip AGENTS.md/CLAUDE.md discovery (no <project_context>)
545 --no-extensions, -ne Skip Rust cdylib and JS/TS extension loading
546 --enable-pi-packages Enable configured Pi JS/TS packages (starts Node)
547 --extensions-dir, -ed <dir> Extra dir to scan for plugins (.dll/.so/.dylib); repeatable
548 (also via RPI_EXTENSIONS_DIR env: ';' on Windows, ':' on Unix)
549 --debug-system-prompt Print the resolved system-prompt sections to stderr (verification)
550 --verbose Show startup warnings (e.g. ignored flags)
551 --help, -h Show this help
552 --version, -v Show version
553
554{u}Subcommands:{r}
555 update Update the rpi CLI from crates.io
556 auth login|check|logout Manage persisted credentials in ~/.rpi/auth.json
557 (see `rpi auth --help`)
558 package list|add|remove|update Manage TS packages and Rust extensions
559 (see `rpi package --help`)
560 install <crate> Build and install a Rust cdylib extension
561 (see `rpi install --help`)
562 install-pi <spec> Install an npm/git/local Pi package
563 (see `rpi install-pi --help`)
564 uninstall <crate> Remove an installed Rust cdylib extension
565 (use `rpi uninstall pi <spec>` for Pi packages)
566 uninstall-pi <spec> Remove an installed npm/git/local Pi package
567 (see `rpi uninstall-pi --help`)
568 dev [options] Build, watch, and hot-reload a Rust extension
569 (see `rpi dev --help`)
570
571{u}Built-in Tools:{r}
572 {builtin} (enabled by default; Pi-compatible default set)
573
574{u}Examples:{r}
575 # Interactive with an initial prompt
576 {name} \"List all .rs files in src/\"
577
578 # Single-shot print mode
579 {name} -p \"Summarize this project\"
580
581 # Include a file in the initial message
582 {name} @README.md \"What does this project do?\"
583
584 # Continue the previous session
585 {name} -c \"What did we discuss?\"
586
587 # Use a specific model + thinking level
588 {name} --model claude-sonnet-5 --thinking high \"Refactor this\"
589
590 # JSON event stream (one JSON object per line on stdout)
591 {name} --mode json -p \"Inspect the code\"
592
593 # Read-only: no file-modifying tools
594 {name} --tools read,bash -p \"Review the code in src/\"
595
596{u}Environment:{r}
597 ANTHROPIC_API_KEY Anthropic API key (x-api-key) — fallback when no stored credential
598 ANTHROPIC_AUTH_TOKEN Bearer token (Authorization: Bearer) for third-party gateways
599 ANTHROPIC_BASE_URL Override the Anthropic endpoint (e.g. a compatible proxy)
600 OPENAI_API_KEY Bearer token for openai-completions/responses
601 RPI_CODING_AGENT_DIR Override the ~/.rpi config directory (auth.json + models.json)
602
603{u}Notes:{r}
604 Supported HTTP protocols are Anthropic Messages and OpenAI Chat Completions.
605 Define custom model catalogs and provider apiKey values in
606 ~/.rpi/agent/models.json. The interactive TUI, Rust and JS/TS extensions,
607 opt-in Pi package resources, skills, prompt templates, themes, model cycling, session
608 fork/export, and trust commands are
609 available in the current build. OAuth, RPC, and full model cycling remain
610 outside the current implementation.
611",
612 name = crate::APP_NAME,
613 builtin = builtin,
614 u = "\x1b[1m",
615 r = "\x1b[0m",
616 );
617}
618
619pub fn print_version() {
621 println!("{} {}", crate::APP_NAME, crate::VERSION);
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 fn s(args: &[&str]) -> Vec<String> {
629 args.iter().map(|a| a.to_string()).collect()
630 }
631
632 #[test]
633 fn parses_basic_prompt() {
634 let a = parse_args(&s(&["hello", "world"]));
635 assert_eq!(a.messages, vec!["hello".to_string(), "world".to_string()]);
636 assert!(!a.help);
637 }
638
639 #[test]
640 fn parses_help_and_version() {
641 let a = parse_args(&s(&["--help"]));
642 assert!(a.help);
643 let a = parse_args(&s(&["-v"]));
644 assert!(a.version);
645 }
646
647 #[test]
648 fn print_consumes_following_positional() {
649 let a = parse_args(&s(&["-p", "summarize"]));
650 assert!(a.print);
651 assert_eq!(a.messages, vec!["summarize".to_string()]);
652 }
653
654 #[test]
655 fn print_does_not_consume_file_or_flag() {
656 let a = parse_args(&s(&["-p", "@file.md"]));
657 assert!(a.print);
658 assert!(a.messages.is_empty());
659 assert_eq!(a.file_args, vec![PathBuf::from("file.md")]);
660 }
661
662 #[test]
663 fn model_and_thinking() {
664 let a = parse_args(&s(&["--model", "claude-sonnet-5", "--thinking", "high"]));
665 assert_eq!(a.model.as_deref(), Some("claude-sonnet-5"));
666 assert_eq!(a.thinking, Some(ThinkingLevel::High));
667 }
668
669 #[test]
670 fn model_with_thinking_shorthand() {
671 let a = parse_args(&s(&["--model", "claude-sonnet-5:high"]));
672 assert_eq!(a.model.as_deref(), Some("claude-sonnet-5:high"));
674 }
675
676 #[test]
677 fn tools_split_csv() {
678 let a = parse_args(&s(&["--tools", "read, bash ,write"]));
679 assert_eq!(
680 a.tools.as_deref(),
681 Some(&["read".to_string(), "bash".to_string(), "write".to_string()][..])
682 );
683 }
684
685 #[test]
686 fn unknown_short_flag_errors() {
687 let a = parse_args(&s(&["-Z"]));
688 assert!(!a.errors.is_empty());
689 }
690
691 #[test]
692 fn unknown_long_flag_warns_not_errors() {
693 let a = parse_args(&s(&["--frobnicate", "value"]));
694 assert!(a.errors.is_empty());
695 assert!(!a.ignored.is_empty());
696 }
697
698 #[test]
699 fn models_flag_parses_csv() {
700 let a = parse_args(&s(&["--models", "a,b,c"]));
701 assert!(a.errors.is_empty());
702 assert!(a.ignored.is_empty(), "--models is implemented");
703 assert_eq!(
704 a.models.as_deref(),
705 Some(&["a".to_string(), "b".to_string(), "c".to_string()][..])
706 );
707 assert!(a.messages.is_empty());
709 }
710
711 #[test]
712 fn list_models_accepts_bare_and_search_forms() {
713 let bare = parse_args(&s(&["--list-models"]));
714 assert_eq!(bare.list_models.as_deref(), Some(""));
715 assert!(bare.ignored.is_empty());
716 assert!(bare.messages.is_empty());
717
718 let search = parse_args(&s(&["--list-models", "claude"]));
719 assert_eq!(search.list_models.as_deref(), Some("claude"));
720 assert!(search.messages.is_empty());
721
722 let inline = parse_args(&s(&["--list-models=gpt"]));
723 assert_eq!(inline.list_models.as_deref(), Some("gpt"));
724 }
725
726 #[test]
727 fn offline_flag_is_honored_without_warning() {
728 let args = parse_args(&s(&["--offline"]));
729 assert!(args.offline);
730 assert!(args.ignored.is_empty());
731 }
732
733 #[test]
734 fn project_trust_flags_are_honored_without_warning() {
735 let approved = parse_args(&s(&["--approve"]));
736 assert_eq!(approved.trust_override, Some(true));
737 assert!(approved.ignored.is_empty());
738 let denied = parse_args(&s(&["--no-approve"]));
739 assert_eq!(denied.trust_override, Some(false));
740 assert!(denied.ignored.is_empty());
741 }
742
743 #[test]
744 fn export_flag_captures_input_and_output_position() {
745 let args = parse_args(&s(&["--export", "session.jsonl", "transcript.html"]));
746 assert_eq!(args.export, Some(PathBuf::from("session.jsonl")));
747 assert_eq!(args.messages, vec!["transcript.html".to_string()]);
748 assert!(args.ignored.is_empty());
749 }
750
751 #[test]
752 fn session_id_and_fork_flags_parse() {
753 let a = parse_args(&s(&["--session-id", "01abc", "--fork", "xyz"]));
754 assert!(a.errors.is_empty());
755 assert_eq!(a.session_id.as_deref(), Some("01abc"));
756 assert_eq!(a.fork.as_deref(), Some("xyz"));
757 let a = parse_args(&s(&[
758 "-e",
759 "plugin.dll",
760 "--skill",
761 "s",
762 "--prompt-template",
763 "t.md",
764 ]));
765 assert_eq!(a.extension.len(), 1);
766 assert_eq!(a.skill.len(), 1);
767 assert_eq!(a.prompt_template.len(), 1);
768 }
769
770 #[test]
771 fn no_skills_flag_honored() {
772 let a = parse_args(&s(&["-ns"]));
773 assert!(a.errors.is_empty());
774 assert!(a.no_skills);
775 assert!(a.ignored.is_empty());
777 }
778
779 #[test]
780 fn no_prompt_templates_flag_honored() {
781 let a = parse_args(&s(&["--no-prompt-templates"]));
782 assert!(a.no_prompt_templates);
783 assert!(a.ignored.is_empty());
784 }
785
786 #[test]
787 fn no_context_files_flag_honored() {
788 let a = parse_args(&s(&["-nc"]));
789 assert!(a.no_context_files);
790 assert!(a.ignored.is_empty());
791 }
792
793 #[test]
794 fn no_extensions_flag_honored() {
795 let a = parse_args(&s(&["--no-extensions"]));
797 assert!(a.no_extensions);
798 assert!(a.ignored.is_empty());
799 }
800
801 #[test]
802 fn pi_packages_are_disabled_by_default_and_explicitly_enabled() {
803 let a = parse_args(&s(&[]));
804 assert!(!a.enable_pi_packages);
805 assert!(a.ignored.is_empty());
806
807 let a = parse_args(&s(&["--enable-pi-packages"]));
808 assert!(a.enable_pi_packages);
809 assert!(a.ignored.is_empty());
810 }
811
812 #[test]
813 fn extensions_dir_flag_collects_dirs() {
814 let a = parse_args(&s(&["--extensions-dir", "/a/b", "-ed", "/c/d"]));
815 assert_eq!(
816 a.extensions_dir,
817 vec![PathBuf::from("/a/b"), PathBuf::from("/c/d")]
818 );
819 assert!(a.ignored.is_empty());
820 }
821
822 #[test]
823 fn extensions_dir_inline_equals_form() {
824 let a = parse_args(&s(&["--extensions-dir=/x/y"]));
825 assert_eq!(a.extensions_dir, vec![PathBuf::from("/x/y")]);
826 }
827
828 #[test]
829 fn extensions_dir_env_is_merged() {
830 let a = parse_args(&s(&["--extensions-dir", "/flag/only"]));
836 assert!(a
837 .extensions_dir
838 .iter()
839 .any(|p| p == &PathBuf::from("/flag/only")));
840 }
841
842 #[test]
843 fn file_args_stripped() {
844 let a = parse_args(&s(&["@a.txt", "@b.md", "hi"]));
845 assert_eq!(
846 a.file_args,
847 vec![PathBuf::from("a.txt"), PathBuf::from("b.md")]
848 );
849 assert_eq!(a.messages, vec!["hi".to_string()]);
850 }
851
852 #[test]
853 fn equals_form_supported() {
854 let a = parse_args(&s(&["--model=claude-sonnet-5", "--thinking=low"]));
855 assert_eq!(a.model.as_deref(), Some("claude-sonnet-5"));
856 assert_eq!(a.thinking, Some(ThinkingLevel::Low));
857 }
858
859 #[test]
860 fn theme_flag_is_honored() {
861 let a = parse_args(&s(&["--theme", "ocean.json"]));
862 assert_eq!(a.theme.as_deref(), Some("ocean.json"));
863 assert!(a.ignored.is_empty());
864 }
865
866 #[test]
867 fn no_themes_is_honored() {
868 let a = parse_args(&s(&["--no-themes"]));
869 assert!(a.no_themes);
870 assert!(a.ignored.is_empty());
871 }
872
873 #[test]
874 fn tui_mode_parses_and_validates() {
875 assert_eq!(
876 parse_args(&s(&["--tui-mode", "regular"])).tui_mode,
877 TuiMode::Regular
878 );
879 assert_eq!(
880 parse_args(&s(&["--tui-mode=fullscreen"])).tui_mode,
881 TuiMode::Fullscreen
882 );
883 let invalid = parse_args(&s(&["--tui-mode", "split"]));
884 assert!(!invalid.errors.is_empty());
885 }
886
887 #[test]
888 fn resolve_mode_interactive_when_tty() {
889 let a = Args {
890 print: true,
891 ..Args::default()
892 };
893 assert_eq!(resolve_mode(&a, true, true), RunMode::Print);
894 let a = Args::default();
895 assert_eq!(resolve_mode(&a, true, true), RunMode::Interactive);
896 let a = Args {
897 mode: Mode::Json,
898 ..Args::default()
899 };
900 assert_eq!(resolve_mode(&a, true, true), RunMode::Json);
901 let a = Args {
902 mode: Mode::Rpc,
903 ..Args::default()
904 };
905 assert_eq!(resolve_mode(&a, true, true), RunMode::Rpc);
906 }
907
908 #[test]
909 fn piped_stdout_forces_print() {
910 let a = Args::default();
911 assert_eq!(resolve_mode(&a, true, false), RunMode::Print);
913 }
914}