oxicode/cli/commands/
issue.rs1use crate::cli::IssueCommands;
4use anyhow::Result;
5
6pub async fn handle_issue(action: &IssueCommands) -> Result<()> {
9 use oxicode_sdk::format_issue_full;
10 use oxicode_sdk::{IssueFilter, Priority, Status};
11
12 let cwd = std::env::current_dir()?;
13
14 if let IssueCommands::Reap = action {
18 let dir = oxicode_sdk::issues_dir(&cwd);
19 let removed = oxicode_sdk::liveness::reap_orphans(&dir)
20 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
21 println!("reaped {removed} dead alive-lock file(s)");
22 return Ok(());
23 }
24
25 let store = oxicode_sdk::FileIssueStore::open_from_cwd(&cwd)?;
26
27 match action {
28 IssueCommands::List { all, label, text } => {
29 let filter = IssueFilter {
30 status: if *all { None } else { Some(Status::Open) },
31 priority: None,
32 label: label.clone(),
33 assigned_to_session: None,
34 text: text.clone(),
35 };
36 let issues = store.list(&filter)?;
37 if issues.is_empty() {
38 println!("(no issues)");
39 } else {
40 for i in &issues {
41 println!("{}", oxicode_sdk::format_issue_line(i));
42 }
43 }
44 }
45 IssueCommands::Show { id } => {
46 let (issue, hash) = store.read(*id)?;
47 println!("{}", format_issue_full(&issue, &hash));
48 }
49 IssueCommands::New {
50 title,
51 body,
52 priority,
53 labels,
54 } => {
55 let body = body.clone().unwrap_or_default();
56 let prio = match priority.as_deref() {
57 Some("low") => Priority::Low,
58 Some("medium") | None => Priority::Medium,
59 Some("high") => Priority::High,
60 Some("critical") => Priority::Critical,
61 Some(other) => anyhow::bail!("invalid priority: {other}"),
62 };
63 let labels: Vec<String> = labels
64 .as_deref()
65 .map(|s| s.split(',').map(|l| l.trim().to_string()).collect())
66 .unwrap_or_default();
67 let issue = store.create(title.clone(), body, prio, labels, None)?;
68 println!("created issue #{}: {}", issue.meta.id, issue.meta.title);
69 }
70 IssueCommands::Close { id, hash } => {
71 let session = format!(
72 "cli-{}-{}",
73 std::process::id(),
74 std::time::SystemTime::now()
75 .duration_since(std::time::UNIX_EPOCH)
76 .map(|d| d.as_nanos())
77 .unwrap_or(0)
78 );
79 let _guard = oxicode_sdk::liveness::acquire(&store.issues_dir(), &session).ok();
80 let (issue, current_hash) = store.read(*id)?;
81 if let Some(a) = &issue.meta.assigned_to
82 && a.session != session
83 && oxicode_sdk::liveness::is_session_alive(&store.issues_dir(), &a.session)
84 {
85 anyhow::bail!(
86 "issue #{id} is currently being worked on by session {} (since {}); cannot close from CLI",
87 a.session,
88 a.acquired_at,
89 );
90 }
91 let effective_hash = hash.clone().unwrap_or(current_hash);
92 store
93 .start(*id, &session, Some(effective_hash))
94 .await
95 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
96 let (_, fresh_hash) = store.read(*id)?;
97 let closed = store
98 .close(*id, &session, Some(fresh_hash))
99 .await
100 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
101 println!("closed issue #{}: {}", closed.meta.id, closed.meta.title);
102 }
103 IssueCommands::Reopen { id, hash } => {
104 let effective_hash = match hash.clone() {
105 Some(h) => h,
106 None => store.read(*id)?.1,
107 };
108 let reopened = store
109 .reopen(*id, Some(effective_hash))
110 .await
111 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
112 println!(
113 "reopened issue #{}: {}",
114 reopened.meta.id, reopened.meta.title
115 );
116 }
117 IssueCommands::Reap => unreachable!("reap handled before store open"),
118 }
119 Ok(())
120}