Skip to main content

aster_cli/commands/
project.rs

1use 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
9/// Format a DateTime for display
10fn format_date(date: DateTime<chrono::Utc>) -> String {
11    // Format: "2025-05-08 18:15:30"
12    date.format("%Y-%m-%d %H:%M:%S").to_string()
13}
14
15/// Handle the default project command
16///
17/// Offers options to resume the most recently accessed project
18pub fn handle_project_default() -> Result<()> {
19    let tracker = ProjectTracker::load()?;
20    let mut projects = tracker.list_projects();
21
22    if projects.is_empty() {
23        // If no projects exist, just start a new one in the current directory
24        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    // Sort projects by last_accessed (newest first)
36    projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
37
38    // Get the most recent project
39    let project = &projects[0];
40    let project_dir = &project.path;
41
42    // Check if the directory exists
43    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    // Format the path for display
52    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    // Ask the user what they want to do
68    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            // Get the session ID if available
99            let session_id = project.last_session_id.clone();
100
101            // Change to the project directory
102            std::env::set_current_dir(project_dir)?;
103
104            // Build the command to run aster
105            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            // Execute the command
114            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            // Change to the project directory
127            std::env::set_current_dir(project_dir)?;
128
129            // Build the command to run aster with a fresh session
130            let mut command = std::process::Command::new("aster");
131            command.arg("session");
132
133            // Execute the command
134            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            // Build the command to run aster
144            let mut command = std::process::Command::new("aster");
145            command.arg("session");
146
147            // Execute the command
148            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
162/// Handle the interactive projects command
163///
164/// Shows a list of projects and lets the user select one to resume
165pub 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    // Sort projects by last_accessed (newest first)
175    projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
176
177    // Format project paths for display
178    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            // Include last instruction if available (truncated)
198            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), // Value to return
210                format!("{} ({}){}", short_path, formatted_date, instruction_preview), // Display text with instruction
211            )
212        })
213        .collect();
214
215    // Let the user select a project
216    let _ = intro("aster Project Manager");
217    let mut select = cliclack::select("Select a project:");
218
219    // Add each project as an option
220    for (value, display) in &project_choices {
221        select = select.item(value, display, "");
222    }
223
224    // Add a cancel option
225    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    // Parse the selected index
236    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    // Get the selected project
243    let project = &projects[index - 1];
244    let project_dir = &project.path;
245
246    // Check if the directory exists
247    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    // Ask if the user wants to resume the session or start a new one
256    let session_id = project.last_session_id.clone();
257    let has_previous_session = session_id.is_some();
258
259    // Change to the project directory first
260    std::env::set_current_dir(project_dir)?;
261    let _ = outro(format!("Changed to directory: {}", project_dir));
262
263    // Only ask about resuming if there's a previous session
264    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    // Build the command to run aster
284    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    // Execute the command
297    let status = command.status()?;
298
299    if !status.success() {
300        println!("Failed to run aster. Exit code: {:?}", status.code());
301    }
302
303    Ok(())
304}