raps_cli/commands/
interactive.rs1use anyhow::{Context, Result};
7use dialoguer::Select;
8use raps_acc::RfiClient;
9use raps_dm::DataManagementClient;
10pub use raps_kernel::interactive::is_non_interactive;
11use raps_kernel::{api_health, progress};
12
13pub async fn prompt_for_hub(client: &DataManagementClient) -> Result<String> {
14 if is_non_interactive() {
15 anyhow::bail!(
16 "Hub ID is required in non-interactive mode. Please provide it as an argument."
17 );
18 }
19
20 let spinner = progress::spinner("Fetching hubs...");
21 let start = std::time::Instant::now();
22 let hubs = client.list_hubs().await.context(
23 "Failed to list hubs. This requires 3-legged auth \u{2014} run 'raps auth login' first",
24 );
25 let elapsed = start.elapsed();
26 let snap = api_health::snapshot();
27 let suffix = if snap.sample_count > 0 {
28 format!(
29 " ({}, avg: {}, API: {})",
30 api_health::format_duration_ms(elapsed),
31 api_health::format_duration_ms(snap.avg_latency),
32 snap.health_status,
33 )
34 } else {
35 format!(" ({})", api_health::format_duration_ms(elapsed))
36 };
37 match &hubs {
38 Ok(_) => spinner.finish_with_message(format!("\u{2713} Fetching hubs{}", suffix)),
39 Err(_) => spinner.finish_with_message(format!(
40 "\u{2717} Fetching hubs (after {})",
41 api_health::format_duration_ms(elapsed)
42 )),
43 }
44 let hubs = hubs?;
45
46 if hubs.is_empty() {
47 anyhow::bail!("No hubs found. Make sure you're logged in with 3-legged auth.");
48 }
49
50 let hub_names: Vec<String> = hubs
51 .iter()
52 .map(|h| format!("{} ({})", h.attributes.name, h.id))
53 .collect();
54
55 let selection = Select::new()
56 .with_prompt("Select a Hub")
57 .items(&hub_names)
58 .interact()?;
59
60 Ok(hubs[selection].id.clone())
61}
62
63pub async fn prompt_for_project(client: &DataManagementClient, hub_id: &str) -> Result<String> {
64 if is_non_interactive() {
65 anyhow::bail!(
66 "Project ID is required in non-interactive mode. Please provide it as an argument."
67 );
68 }
69
70 let spinner = progress::spinner("Fetching projects...");
71 let start = std::time::Instant::now();
72 let projects = client
73 .list_projects(hub_id)
74 .await
75 .context(format!("Failed to list projects in hub '{}'", hub_id));
76 let elapsed = start.elapsed();
77 let snap = api_health::snapshot();
78 let suffix = if snap.sample_count > 0 {
79 format!(
80 " ({}, avg: {}, API: {})",
81 api_health::format_duration_ms(elapsed),
82 api_health::format_duration_ms(snap.avg_latency),
83 snap.health_status,
84 )
85 } else {
86 format!(" ({})", api_health::format_duration_ms(elapsed))
87 };
88 match &projects {
89 Ok(_) => spinner.finish_with_message(format!("\u{2713} Fetching projects{}", suffix)),
90 Err(_) => spinner.finish_with_message(format!(
91 "\u{2717} Fetching projects (after {})",
92 api_health::format_duration_ms(elapsed)
93 )),
94 }
95 let projects = projects?;
96
97 if projects.is_empty() {
98 anyhow::bail!("No projects found in this hub.");
99 }
100
101 let project_names: Vec<String> = projects
102 .iter()
103 .map(|p| format!("{} ({})", p.attributes.name, p.id))
104 .collect();
105
106 let selection = Select::new()
107 .with_prompt("Select a Project")
108 .items(&project_names)
109 .interact()?;
110
111 Ok(projects[selection].id.clone())
112}
113
114pub async fn prompt_for_folder(
115 client: &DataManagementClient,
116 hub_id: &str,
117 project_id: &str,
118) -> Result<String> {
119 if is_non_interactive() {
120 anyhow::bail!(
121 "Folder ID is required in non-interactive mode. Please provide it as an argument."
122 );
123 }
124
125 let spinner = progress::spinner("Fetching top folders...");
126 let start = std::time::Instant::now();
127 let folders = client
128 .get_top_folders(hub_id, project_id)
129 .await
130 .context(format!(
131 "Failed to get top folders for project '{}'",
132 project_id
133 ));
134 let elapsed = start.elapsed();
135 let snap = api_health::snapshot();
136 let suffix = if snap.sample_count > 0 {
137 format!(
138 " ({}, avg: {}, API: {})",
139 api_health::format_duration_ms(elapsed),
140 api_health::format_duration_ms(snap.avg_latency),
141 snap.health_status,
142 )
143 } else {
144 format!(" ({})", api_health::format_duration_ms(elapsed))
145 };
146 match &folders {
147 Ok(_) => spinner.finish_with_message(format!("\u{2713} Fetching top folders{}", suffix)),
148 Err(_) => spinner.finish_with_message(format!(
149 "\u{2717} Fetching top folders (after {})",
150 api_health::format_duration_ms(elapsed)
151 )),
152 }
153 let folders = folders?;
154
155 if folders.is_empty() {
156 anyhow::bail!("No folders found in this project.");
157 }
158
159 let folder_names: Vec<String> = folders
160 .iter()
161 .map(|f| {
162 let name = f
163 .attributes
164 .display_name
165 .as_deref()
166 .unwrap_or(f.attributes.name.as_str());
167 format!("{} ({})", name, f.id)
168 })
169 .collect();
170
171 let selection = Select::new()
172 .with_prompt("Select a Folder")
173 .items(&folder_names)
174 .interact()?;
175
176 Ok(folders[selection].id.clone())
177}
178
179pub async fn prompt_for_rfi(client: &RfiClient, project_id: &str) -> Result<String> {
180 if is_non_interactive() {
181 anyhow::bail!(
182 "RFI ID is required in non-interactive mode. Please provide it as an argument."
183 );
184 }
185
186 let spinner = progress::spinner("Fetching RFIs...");
187 let start = std::time::Instant::now();
188 let rfis = client
189 .list_rfis(project_id)
190 .await
191 .context(format!("Failed to list RFIs for project '{}'", project_id));
192 let elapsed = start.elapsed();
193 let snap = api_health::snapshot();
194 let suffix = if snap.sample_count > 0 {
195 format!(
196 " ({}, avg: {}, API: {})",
197 api_health::format_duration_ms(elapsed),
198 api_health::format_duration_ms(snap.avg_latency),
199 snap.health_status,
200 )
201 } else {
202 format!(" ({})", api_health::format_duration_ms(elapsed))
203 };
204 match &rfis {
205 Ok(_) => spinner.finish_with_message(format!("\u{2713} Fetching RFIs{}", suffix)),
206 Err(_) => spinner.finish_with_message(format!(
207 "\u{2717} Fetching RFIs (after {})",
208 api_health::format_duration_ms(elapsed)
209 )),
210 }
211 let rfis = rfis?;
212
213 if rfis.is_empty() {
214 anyhow::bail!("No RFIs found in this project.");
215 }
216
217 let rfi_names: Vec<String> = rfis
218 .iter()
219 .map(|r| {
220 let num = r.number.as_deref().unwrap_or("-");
221 format!("[{}] {} ({}) - {}", num, r.title, r.id, r.status)
222 })
223 .collect();
224
225 let selection = Select::new()
226 .with_prompt("Select an RFI")
227 .items(&rfi_names)
228 .interact()?;
229
230 Ok(rfis[selection].id.clone())
231}