1use std::path::{Path, PathBuf};
2
3use crate::terminals;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct LaunchRequest {
7 pub cwd_path: PathBuf,
8 pub prompt: Option<String>,
9 pub resume: Option<String>,
10}
11
12impl LaunchRequest {
13 pub fn option_summary(&self) -> String {
14 let mut parts = Vec::new();
15 if let Some(resume) = &self.resume {
16 parts.push(format!("resume {resume}"));
17 }
18 if self.prompt.is_some() {
19 parts.push("prompt set".to_string());
20 }
21
22 if parts.is_empty() {
23 String::new()
24 } else {
25 format!(" ({})", parts.join(", "))
26 }
27 }
28}
29
30pub fn prepare(
31 cwd: &str,
32 prompt: Option<&str>,
33 resume: Option<&str>,
34) -> Result<LaunchRequest, String> {
35 let raw_cwd = if cwd.trim().is_empty() {
36 "."
37 } else {
38 cwd.trim()
39 };
40 let cwd_path = Path::new(raw_cwd)
41 .canonicalize()
42 .map_err(|_| format!("Directory not found: {raw_cwd}"))?;
43
44 if !cwd_path.is_dir() {
45 return Err(format!(
46 "Launch path is not a directory: {}",
47 cwd_path.display()
48 ));
49 }
50
51 let prompt = prompt
52 .filter(|value| !value.trim().is_empty())
53 .map(ToOwned::to_owned);
54 let resume = resume
55 .map(str::trim)
56 .filter(|value| !value.is_empty())
57 .map(ToOwned::to_owned);
58
59 Ok(LaunchRequest {
60 cwd_path,
61 prompt,
62 resume,
63 })
64}
65
66pub fn launch(request: &LaunchRequest) -> Result<String, String> {
67 terminals::launch_session(
68 request.cwd_path.to_string_lossy().as_ref(),
69 request.prompt.as_deref(),
70 request.resume.as_deref(),
71 )
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn prepare_normalizes_optional_fields() {
80 let temp = tempfile::tempdir().unwrap();
81 let request = prepare(
82 temp.path().to_string_lossy().as_ref(),
83 Some("ship it"),
84 Some(" session-123 "),
85 )
86 .unwrap();
87
88 assert_eq!(request.cwd_path, temp.path().canonicalize().unwrap());
89 assert_eq!(request.prompt.as_deref(), Some("ship it"));
90 assert_eq!(request.resume.as_deref(), Some("session-123"));
91 }
92
93 #[test]
94 fn prepare_rejects_missing_directory() {
95 let missing = "/tmp/claudectl-this-path-should-not-exist";
96 let err = prepare(missing, None, None).unwrap_err();
97 assert_eq!(err, format!("Directory not found: {missing}"));
98 }
99
100 #[test]
101 fn prepare_treats_blank_prompt_and_resume_as_none() {
102 let temp = tempfile::tempdir().unwrap();
103 let request = prepare(
104 temp.path().to_string_lossy().as_ref(),
105 Some(" "),
106 Some(""),
107 )
108 .unwrap();
109
110 assert_eq!(request.prompt, None);
111 assert_eq!(request.resume, None);
112 }
113}