acp_cli/cli/
prompt_source.rs1use std::io::Read;
2use std::path::Path;
3
4use crate::error::{AcpCliError, Result};
5
6pub fn resolve_prompt(
15 file_flag: Option<&str>,
16 positional: &[String],
17 stdin_is_terminal: bool,
18) -> Result<String> {
19 match file_flag {
20 Some(path) => {
21 if !positional.is_empty() {
23 return Err(AcpCliError::Usage(
24 "cannot combine --file with positional prompt arguments".into(),
25 ));
26 }
27 if path == "-" {
28 read_stdin()
29 } else {
30 read_file(path)
31 }
32 }
33 None => {
34 if !stdin_is_terminal && positional.is_empty() {
35 read_stdin()
37 } else {
38 Ok(positional.join(" "))
39 }
40 }
41 }
42}
43
44fn read_stdin() -> Result<String> {
45 let mut input = String::new();
46 std::io::stdin().read_to_string(&mut input)?;
47 Ok(input.trim_end().to_string())
48}
49
50fn read_file(path: &str) -> Result<String> {
51 let p = Path::new(path);
52 if !p.exists() {
53 return Err(AcpCliError::Usage(format!("file not found: {path}")));
54 }
55 let content = std::fs::read_to_string(p)?;
56 Ok(content.trim_end().to_string())
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn positional_prompt_joined() {
65 let result = resolve_prompt(None, &["hello".into(), "world".into()], true).unwrap();
66 assert_eq!(result, "hello world");
67 }
68
69 #[test]
70 fn file_flag_and_positional_is_error() {
71 let result = resolve_prompt(Some("prompt.txt"), &["extra".into()], true);
72 assert!(result.is_err());
73 let msg = result.unwrap_err().to_string();
74 assert!(msg.contains("cannot combine"));
75 }
76
77 #[test]
78 fn file_flag_reads_file() {
79 let dir = tempfile::tempdir().unwrap();
80 let file_path = dir.path().join("prompt.txt");
81 std::fs::write(&file_path, "prompt from file\n").unwrap();
82
83 let result = resolve_prompt(Some(file_path.to_str().unwrap()), &[], true).unwrap();
84 assert_eq!(result, "prompt from file");
85 }
86
87 #[test]
88 fn file_flag_missing_file_is_error() {
89 let result = resolve_prompt(Some("/tmp/nonexistent-acp-cli-test-file.txt"), &[], true);
90 assert!(result.is_err());
91 let msg = result.unwrap_err().to_string();
92 assert!(msg.contains("file not found"));
93 }
94
95 #[test]
96 fn no_file_no_positional_terminal_gives_empty() {
97 let result = resolve_prompt(None, &[], true).unwrap();
98 assert_eq!(result, "");
99 }
100
101 #[test]
102 fn file_dash_with_positional_is_error() {
103 let result = resolve_prompt(Some("-"), &["extra".into()], true);
104 assert!(result.is_err());
105 }
106
107 #[test]
108 fn file_trims_trailing_newlines() {
109 let dir = tempfile::tempdir().unwrap();
110 let path = dir.path().join("prompt.txt");
111 std::fs::write(&path, "hello\n\n\n").unwrap();
112
113 let result = resolve_prompt(Some(path.to_str().unwrap()), &[], true).unwrap();
114 assert_eq!(result, "hello");
115 }
116
117 #[test]
118 fn file_preserves_internal_newlines() {
119 let dir = tempfile::tempdir().unwrap();
120 let path = dir.path().join("prompt.txt");
121 std::fs::write(&path, "line one\nline two\n").unwrap();
122
123 let result = resolve_prompt(Some(path.to_str().unwrap()), &[], true).unwrap();
124 assert_eq!(result, "line one\nline two");
125 }
126}