aster_cli/commands/
project.rs1use anyhow::Result;
2use chrono::DateTime;
3use cliclack::{self, intro, outro};
4use std::path::Path;
5
6use crate::project_tracker::ProjectTracker;
7use aster::utils::safe_truncate;
8
9fn format_date(date: DateTime<chrono::Utc>) -> String {
11 date.format("%Y-%m-%d %H:%M:%S").to_string()
13}
14
15pub fn handle_project_default() -> Result<()> {
19 let tracker = ProjectTracker::load()?;
20 let mut projects = tracker.list_projects();
21
22 if projects.is_empty() {
23 println!("No previous projects found. Starting a new session in the current directory.");
25 let mut command = std::process::Command::new("aster");
26 command.arg("session");
27 let status = command.status()?;
28
29 if !status.success() {
30 println!("Failed to run aster. Exit code: {:?}", status.code());
31 }
32 return Ok(());
33 }
34
35 projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
37
38 let project = &projects[0];
40 let project_dir = &project.path;
41
42 if !Path::new(project_dir).exists() {
44 println!(
45 "Most recent project directory '{}' no longer exists.",
46 project_dir
47 );
48 return Ok(());
49 }
50
51 let path = Path::new(project_dir);
53 let components: Vec<_> = path.components().collect();
54 let len = components.len();
55 let short_path = if len <= 2 {
56 project_dir.clone()
57 } else {
58 let mut path_str = String::new();
59 path_str.push_str("...");
60 for component in components.iter().skip(len - 2) {
61 path_str.push('/');
62 path_str.push_str(component.as_os_str().to_string_lossy().as_ref());
63 }
64 path_str
65 };
66
67 let _ = intro("aster Project Manager");
69
70 let current_dir = std::env::current_dir()?;
71 let current_dir_display = current_dir.display();
72
73 let choice = cliclack::select("Choose an option:")
74 .item(
75 "resume",
76 format!("Resume project with session: {}", short_path),
77 "Continue with the previous session",
78 )
79 .item(
80 "fresh",
81 format!("Resume project with fresh session: {}", short_path),
82 "Change to the project directory but start a new session",
83 )
84 .item(
85 "new",
86 format!(
87 "Start new project in current directory: {}",
88 current_dir_display
89 ),
90 "Stay in the current directory and start a new session",
91 )
92 .interact()?;
93
94 match choice {
95 "resume" => {
96 let _ = outro(format!("Changing to directory: {}", project_dir));
97
98 let session_id = project.last_session_id.clone();
100
101 std::env::set_current_dir(project_dir)?;
103
104 let mut command = std::process::Command::new("aster");
106 command.arg("session");
107
108 if let Some(id) = session_id {
109 command.arg("--name").arg(&id).arg("--resume");
110 println!("Resuming session: {}", id);
111 }
112
113 let status = command.status()?;
115
116 if !status.success() {
117 println!("Failed to run aster. Exit code: {:?}", status.code());
118 }
119 }
120 "fresh" => {
121 let _ = outro(format!(
122 "Changing to directory: {} with a fresh session",
123 project_dir
124 ));
125
126 std::env::set_current_dir(project_dir)?;
128
129 let mut command = std::process::Command::new("aster");
131 command.arg("session");
132
133 let status = command.status()?;
135
136 if !status.success() {
137 println!("Failed to run aster. Exit code: {:?}", status.code());
138 }
139 }
140 "new" => {
141 let _ = outro("Starting a new session in the current directory");
142
143 let mut command = std::process::Command::new("aster");
145 command.arg("session");
146
147 let status = command.status()?;
149
150 if !status.success() {
151 println!("Failed to run aster. Exit code: {:?}", status.code());
152 }
153 }
154 _ => {
155 let _ = outro("Operation canceled");
156 }
157 }
158
159 Ok(())
160}
161
162pub fn handle_projects_interactive() -> Result<()> {
166 let tracker = ProjectTracker::load()?;
167 let mut projects = tracker.list_projects();
168
169 if projects.is_empty() {
170 println!("No projects found.");
171 return Ok(());
172 }
173
174 projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
176
177 let project_choices: Vec<(String, String)> = projects
179 .iter()
180 .enumerate()
181 .map(|(i, project)| {
182 let path = Path::new(&project.path);
183 let components: Vec<_> = path.components().collect();
184 let len = components.len();
185 let short_path = if len <= 2 {
186 project.path.clone()
187 } else {
188 let mut path_str = String::new();
189 path_str.push_str("...");
190 for component in components.iter().skip(len - 2) {
191 path_str.push('/');
192 path_str.push_str(component.as_os_str().to_string_lossy().as_ref());
193 }
194 path_str
195 };
196
197 let instruction_preview =
199 project
200 .last_instruction
201 .as_ref()
202 .map_or(String::new(), |instr| {
203 let truncated = safe_truncate(instr, 40);
204 format!(" [{}]", truncated)
205 });
206
207 let formatted_date = format_date(project.last_accessed);
208 (
209 format!("{}", i + 1), format!("{} ({}){}", short_path, formatted_date, instruction_preview), )
212 })
213 .collect();
214
215 let _ = intro("aster Project Manager");
217 let mut select = cliclack::select("Select a project:");
218
219 for (value, display) in &project_choices {
221 select = select.item(value, display, "");
222 }
223
224 let cancel_value = String::from("cancel");
226 select = select.item(&cancel_value, "Cancel", "Don't resume any project");
227
228 let selected = select.interact()?;
229
230 if selected == "cancel" {
231 let _ = outro("Project selection canceled.");
232 return Ok(());
233 }
234
235 let index = selected.parse::<usize>().unwrap_or(0);
237 if index == 0 || index > projects.len() {
238 let _ = outro("Invalid selection.");
239 return Ok(());
240 }
241
242 let project = &projects[index - 1];
244 let project_dir = &project.path;
245
246 if !Path::new(project_dir).exists() {
248 let _ = outro(format!(
249 "Project directory '{}' no longer exists.",
250 project_dir
251 ));
252 return Ok(());
253 }
254
255 let session_id = project.last_session_id.clone();
257 let has_previous_session = session_id.is_some();
258
259 std::env::set_current_dir(project_dir)?;
261 let _ = outro(format!("Changed to directory: {}", project_dir));
262
263 let resume_session = if has_previous_session {
265 let session_choice = cliclack::select("What would you like to do?")
266 .item(
267 "resume",
268 "Resume previous session",
269 "Continue with the previous session",
270 )
271 .item(
272 "new",
273 "Start new session",
274 "Start a fresh session in this project directory",
275 )
276 .interact()?;
277
278 session_choice == "resume"
279 } else {
280 false
281 };
282
283 let mut command = std::process::Command::new("aster");
285 command.arg("session");
286
287 if resume_session {
288 if let Some(id) = session_id {
289 command.arg("--name").arg(&id).arg("--resume");
290 println!("Resuming session: {}", id);
291 }
292 } else {
293 println!("Starting new session");
294 }
295
296 let status = command.status()?;
298
299 if !status.success() {
300 println!("Failed to run aster. Exit code: {:?}", status.code());
301 }
302
303 Ok(())
304}