Skip to main content

acp_cli/cli/
prompt_source.rs

1use std::io::Read;
2use std::path::Path;
3
4use crate::error::{AcpCliError, Result};
5
6/// Resolve the prompt text from the available sources.
7///
8/// Priority / conflict rules:
9/// 1. `--file` and positional prompt args are mutually exclusive (Usage error).
10/// 2. `--file -` reads all of stdin.
11/// 3. `--file <path>` reads the file at that path.
12/// 4. If no `--file` but stdin is piped (not a TTY), read stdin as the prompt.
13/// 5. Otherwise, join the positional prompt words.
14pub 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            // --file provided: positional prompt args must be empty
22            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                // stdin is piped and no positional args
36                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}