1use crate::cli::SessionCommands;
4use crate::config::{
5 bind_session_to_terminal, clear_status_cache, current_git_branch,
6 default_actor, resolve_db_path, resolve_project, resolve_project_path, resolve_session_or_suggest,
7};
8use crate::error::{Error, Result};
9use crate::storage::SqliteStorage;
10use serde::Serialize;
11use std::path::PathBuf;
12
13#[derive(Serialize)]
15struct SessionListOutput {
16 sessions: Vec<crate::storage::Session>,
17 count: usize,
18}
19
20pub fn execute(
26 command: &SessionCommands,
27 db_path: Option<&PathBuf>,
28 actor: Option<&str>,
29 session_id: Option<&str>,
30 json: bool,
31) -> Result<()> {
32 let db_path = resolve_db_path(db_path.map(|p| p.as_path()))
33 .ok_or_else(|| Error::NotInitialized)?;
34
35 if !db_path.exists() {
36 return Err(Error::NotInitialized);
37 }
38
39 let actor = actor
40 .map(ToString::to_string)
41 .unwrap_or_else(default_actor);
42
43 match command {
44 SessionCommands::Start {
45 name,
46 description,
47 project,
48 channel,
49 force_new,
50 } => start(
51 &db_path,
52 name,
53 description.as_deref(),
54 project.as_deref(),
55 channel.as_deref(),
56 *force_new,
57 &actor,
58 json,
59 ),
60 SessionCommands::End => end(&db_path, session_id, &actor, json),
61 SessionCommands::Pause => pause(&db_path, session_id, &actor, json),
62 SessionCommands::Resume { id } => resume(&db_path, id, &actor, json),
63 SessionCommands::List {
64 status,
65 limit,
66 search,
67 project,
68 all_projects,
69 include_completed,
70 } => list(
71 &db_path,
72 status,
73 *limit,
74 search.as_deref(),
75 project.as_deref(),
76 *all_projects,
77 *include_completed,
78 json,
79 ),
80 SessionCommands::Switch { id } => switch(&db_path, id, &actor, json),
81 SessionCommands::Rename { name } => rename(&db_path, session_id, name, &actor, json),
82 SessionCommands::Delete { id, force } => delete(&db_path, id, *force, &actor, json),
83 SessionCommands::AddPath { id, path } => {
84 add_path(&db_path, id.as_deref(), path.as_deref(), &actor, json)
85 }
86 SessionCommands::RemovePath { id, path } => {
87 remove_path(&db_path, id.as_deref(), path, &actor, json)
88 }
89 }
90}
91
92fn start(
94 db_path: &PathBuf,
95 name: &str,
96 description: Option<&str>,
97 project: Option<&str>,
98 channel: Option<&str>,
99 force_new: bool,
100 actor: &str,
101 json: bool,
102) -> Result<()> {
103 let mut storage = SqliteStorage::open(db_path)?;
104
105 let resolved = resolve_project(&storage, project)?;
107 let project_path = resolved.project_path;
108 let branch = current_git_branch();
109
110 let resolved_channel = channel
112 .map(ToString::to_string)
113 .or_else(|| branch.clone());
114
115 if !force_new {
117 let existing = storage.list_sessions(Some(&project_path), Some("paused"), Some(10))?;
119 if let Some(session) = existing.iter().find(|s| s.name == name) {
120 storage.update_session_status(&session.id, "active", actor)?;
122
123 bind_session_to_terminal(&session.id, &session.name, &project_path, "active");
125
126 if crate::is_silent() {
127 println!("{}", session.id);
128 return Ok(());
129 }
130
131 if json {
132 let output = serde_json::json!({
133 "id": session.id,
134 "name": session.name,
135 "status": "active",
136 "project_path": session.project_path,
137 "branch": branch,
138 "resumed": true
139 });
140 println!("{output}");
141 } else {
142 println!("Resumed session: {name}");
143 println!(" ID: {}", session.id);
144 println!(" Project: {project_path}");
145 if let Some(ref branch) = branch {
146 println!(" Branch: {branch}");
147 }
148 }
149 return Ok(());
150 }
151 }
152
153 let id = format!("sess_{}", &uuid::Uuid::new_v4().to_string()[..12]);
155
156 storage.create_session(
157 &id,
158 name,
159 description,
160 Some(&project_path),
161 resolved_channel.as_deref(),
162 actor,
163 )?;
164
165 bind_session_to_terminal(&id, name, &project_path, "active");
167
168 if crate::is_silent() {
169 println!("{id}");
170 return Ok(());
171 }
172
173 if json {
174 let output = serde_json::json!({
175 "id": id,
176 "name": name,
177 "status": "active",
178 "project_path": project_path,
179 "branch": branch,
180 "resumed": false
181 });
182 println!("{output}");
183 } else {
184 println!("Started session: {name}");
185 println!(" ID: {id}");
186 println!(" Project: {project_path}");
187 if let Some(ref branch) = branch {
188 println!(" Branch: {branch}");
189 }
190 }
191
192 Ok(())
193}
194
195fn end(db_path: &PathBuf, session_id: Option<&str>, actor: &str, json: bool) -> Result<()> {
197 let mut storage = SqliteStorage::open(db_path)?;
198
199 let sid = resolve_session_or_suggest(session_id, &storage)?;
200 let session = storage
201 .get_session(&sid)?
202 .ok_or_else(|| Error::SessionNotFound { id: sid })?;
203
204 storage.update_session_status(&session.id, "completed", actor)?;
205
206 clear_status_cache();
208
209 if json {
210 let output = serde_json::json!({
211 "id": session.id,
212 "name": session.name,
213 "status": "completed"
214 });
215 println!("{output}");
216 } else {
217 println!("Completed session: {}", session.name);
218 }
219
220 Ok(())
221}
222
223fn pause(db_path: &PathBuf, session_id: Option<&str>, actor: &str, json: bool) -> Result<()> {
225 let mut storage = SqliteStorage::open(db_path)?;
226
227 let sid = resolve_session_or_suggest(session_id, &storage)?;
228 let session = storage
229 .get_session(&sid)?
230 .ok_or_else(|| Error::SessionNotFound { id: sid })?;
231
232 storage.update_session_status(&session.id, "paused", actor)?;
233
234 clear_status_cache();
236
237 if json {
238 let output = serde_json::json!({
239 "id": session.id,
240 "name": session.name,
241 "status": "paused"
242 });
243 println!("{output}");
244 } else {
245 println!("Paused session: {}", session.name);
246 }
247
248 Ok(())
249}
250
251fn resume(db_path: &PathBuf, id: &str, actor: &str, json: bool) -> Result<()> {
254 let mut storage = SqliteStorage::open(db_path)?;
255
256 let session = storage
258 .get_session(id)?
259 .ok_or_else(|| {
260 let all_ids = storage.get_all_session_ids().unwrap_or_default();
261 let similar = crate::validate::find_similar_ids(id, &all_ids, 3);
262 if similar.is_empty() {
263 Error::SessionNotFound { id: id.to_string() }
264 } else {
265 Error::SessionNotFoundSimilar {
266 id: id.to_string(),
267 similar,
268 }
269 }
270 })?;
271
272 storage.update_session_status(id, "active", actor)?;
277
278 let project_path = session
280 .project_path
281 .as_deref()
282 .unwrap_or(".");
283 bind_session_to_terminal(&session.id, &session.name, project_path, "active");
284
285 if json {
286 let output = serde_json::json!({
287 "id": session.id,
288 "name": session.name,
289 "status": "active"
290 });
291 println!("{output}");
292 } else {
293 println!("Resumed session: {}", session.name);
294 }
295
296 Ok(())
297}
298
299#[allow(clippy::too_many_arguments)]
301fn list(
302 db_path: &PathBuf,
303 status: &str,
304 limit: usize,
305 search: Option<&str>,
306 project: Option<&str>,
307 all_projects: bool,
308 include_completed: bool,
309 json: bool,
310) -> Result<()> {
311 let storage = SqliteStorage::open(db_path)?;
312
313 let (effective_status, effective_all_projects) = if search.is_some() {
316 let status_was_explicit = status != "active"; let project_was_explicit = project.is_some(); (
319 if status_was_explicit { status } else { "all" },
320 if project_was_explicit { all_projects } else { true },
321 )
322 } else {
323 (status, all_projects)
324 };
325
326 let project_path = if effective_all_projects {
331 None
332 } else {
333 project.map(ToString::to_string).or_else(|| {
334 resolve_project_path(&storage, None).ok()
335 })
336 };
337
338 let status_filter = if effective_status == "all" {
342 None
343 } else if include_completed {
344 None
346 } else {
347 Some(effective_status)
348 };
349
350 #[allow(clippy::cast_possible_truncation)]
351 let mut sessions = storage.list_sessions_with_search(
352 project_path.as_deref(),
353 status_filter,
354 Some(limit as u32 * 2), search,
356 )?;
357
358 if effective_status != "all" && include_completed {
361 sessions.retain(|s| s.status == effective_status || s.status == "completed");
362 }
363
364 sessions.truncate(limit);
366
367 if crate::is_csv() {
368 println!("id,name,status,project_path");
369 for s in &sessions {
370 let path = s.project_path.as_deref().unwrap_or("");
371 println!("{},{},{},{}", s.id, crate::csv_escape(&s.name), s.status, crate::csv_escape(path));
372 }
373 } else if json {
374 let output = SessionListOutput {
375 count: sessions.len(),
376 sessions,
377 };
378 println!("{}", serde_json::to_string(&output)?);
379 } else if sessions.is_empty() {
380 println!("No sessions found.");
381 } else {
382 println!("Sessions ({} found):", sessions.len());
383 println!();
384 for session in &sessions {
385 let status_icon = match session.status.as_str() {
386 "active" => "●",
387 "paused" => "◐",
388 "completed" => "○",
389 _ => "?",
390 };
391 println!("{} {} [{}]", status_icon, session.name, session.status);
392 println!(" ID: {}", session.id);
393 if let Some(ref path) = session.project_path {
394 println!(" Project: {path}");
395 }
396 if let Some(ref branch) = session.branch {
397 println!(" Branch: {branch}");
398 }
399 println!();
400 }
401 }
402
403 Ok(())
404}
405
406fn switch(db_path: &PathBuf, id: &str, actor: &str, json: bool) -> Result<()> {
408 let mut storage = SqliteStorage::open(db_path)?;
409
410 if let Some(current_sid) = crate::config::current_session_id() {
412 if current_sid != id {
413 if let Ok(Some(_)) = storage.get_session(¤t_sid) {
415 storage.update_session_status(¤t_sid, "paused", actor)?;
416 }
417 }
418 }
419
420 let target = storage
422 .get_session(id)?
423 .ok_or_else(|| {
424 let all_ids = storage.get_all_session_ids().unwrap_or_default();
425 let similar = crate::validate::find_similar_ids(id, &all_ids, 3);
426 if similar.is_empty() {
427 Error::SessionNotFound { id: id.to_string() }
428 } else {
429 Error::SessionNotFoundSimilar {
430 id: id.to_string(),
431 similar,
432 }
433 }
434 })?;
435
436 if target.status != "active" {
438 storage.update_session_status(id, "active", actor)?;
439 }
440
441 let project_path = target
443 .project_path
444 .as_deref()
445 .unwrap_or(".");
446 bind_session_to_terminal(&target.id, &target.name, project_path, "active");
447
448 if json {
449 let output = serde_json::json!({
450 "id": target.id,
451 "name": target.name,
452 "status": "active"
453 });
454 println!("{output}");
455 } else {
456 println!("Switched to session: {}", target.name);
457 }
458
459 Ok(())
460}
461
462fn rename(db_path: &PathBuf, session_id: Option<&str>, new_name: &str, actor: &str, json: bool) -> Result<()> {
464 let mut storage = SqliteStorage::open(db_path)?;
465
466 let sid = resolve_session_or_suggest(session_id, &storage)?;
467 let session = storage
468 .get_session(&sid)?
469 .ok_or_else(|| Error::SessionNotFound { id: sid })?;
470
471 storage.rename_session(&session.id, new_name, actor)?;
472
473 if let Some(ref path) = session.project_path {
475 bind_session_to_terminal(&session.id, new_name, path, &session.status);
476 }
477
478 if json {
479 let output = serde_json::json!({
480 "id": session.id,
481 "name": new_name,
482 "old_name": session.name
483 });
484 println!("{output}");
485 } else {
486 println!("Renamed session to: {new_name}");
487 }
488
489 Ok(())
490}
491
492fn delete(db_path: &PathBuf, id: &str, force: bool, actor: &str, json: bool) -> Result<()> {
494 let mut storage = SqliteStorage::open(db_path)?;
495
496 let session = storage
498 .get_session(id)?
499 .ok_or_else(|| {
500 let all_ids = storage.get_all_session_ids().unwrap_or_default();
501 let similar = crate::validate::find_similar_ids(id, &all_ids, 3);
502 if similar.is_empty() {
503 Error::SessionNotFound { id: id.to_string() }
504 } else {
505 Error::SessionNotFoundSimilar {
506 id: id.to_string(),
507 similar,
508 }
509 }
510 })?;
511
512 if session.status == "active" && !force {
514 return Err(Error::InvalidSessionStatus {
515 expected: "paused or completed (use --force to delete active session)".to_string(),
516 actual: session.status.clone(),
517 });
518 }
519
520 storage.delete_session(id, actor)?;
522
523 if json {
524 let output = serde_json::json!({
525 "id": session.id,
526 "name": session.name,
527 "deleted": true
528 });
529 println!("{output}");
530 } else {
531 println!("Deleted session: {}", session.name);
532 }
533
534 Ok(())
535}
536
537fn add_path(
539 db_path: &PathBuf,
540 id: Option<&str>,
541 path: Option<&str>,
542 actor: &str,
543 json: bool,
544) -> Result<()> {
545 let mut storage = SqliteStorage::open(db_path)?;
546
547 let session_id = resolve_session_or_suggest(id, &storage)?;
549
550 let project_path = match path {
552 Some(p) => std::path::PathBuf::from(p)
553 .canonicalize()
554 .map(|p| p.to_string_lossy().to_string())
555 .unwrap_or_else(|_| p.to_string()),
556 None => std::env::current_dir()
557 .map(|p| p.to_string_lossy().to_string())
558 .map_err(|e| Error::Io(e))?,
559 };
560
561 let session = storage
563 .get_session(&session_id)?
564 .ok_or_else(|| Error::SessionNotFound {
565 id: session_id.clone(),
566 })?;
567
568 storage.add_session_path(&session_id, &project_path, actor)?;
570
571 if json {
572 let output = serde_json::json!({
573 "session_id": session.id,
574 "session_name": session.name,
575 "path_added": project_path
576 });
577 println!("{output}");
578 } else {
579 println!("Added path to session: {}", session.name);
580 println!(" Path: {project_path}");
581 }
582
583 Ok(())
584}
585
586fn remove_path(
588 db_path: &PathBuf,
589 id: Option<&str>,
590 path: &str,
591 actor: &str,
592 json: bool,
593) -> Result<()> {
594 let mut storage = SqliteStorage::open(db_path)?;
595
596 let session_id = resolve_session_or_suggest(id, &storage)?;
598
599 let session = storage
601 .get_session(&session_id)?
602 .ok_or_else(|| Error::SessionNotFound {
603 id: session_id.clone(),
604 })?;
605
606 let project_path = std::path::PathBuf::from(path)
608 .canonicalize()
609 .map(|p| p.to_string_lossy().to_string())
610 .unwrap_or_else(|_| path.to_string());
611
612 storage.remove_session_path(&session_id, &project_path, actor)?;
614
615 if json {
616 let output = serde_json::json!({
617 "session_id": session.id,
618 "session_name": session.name,
619 "path_removed": project_path
620 });
621 println!("{output}");
622 } else {
623 println!("Removed path from session: {}", session.name);
624 println!(" Path: {project_path}");
625 }
626
627 Ok(())
628}