singularity_cli/commands/
project.rs1use anyhow::Result;
2use chrono_tz::Tz;
3use clap::Subcommand;
4
5use crate::client::ApiClient;
6use crate::models::project::{Project, ProjectCreate, ProjectListResponse, ProjectUpdate};
7
8#[derive(Subcommand)]
9pub enum ProjectCmd {
10 #[command(about = "List all projects")]
11 List {
12 #[arg(long, help = "Maximum number of projects to return (max 1000)")]
13 max_count: Option<u32>,
14 #[arg(long, help = "Number of projects to skip for pagination")]
15 offset: Option<u32>,
16 #[arg(long, help = "Include soft-deleted projects")]
17 include_removed: bool,
18 #[arg(long, help = "Include archived projects")]
19 include_archived: bool,
20 },
21 #[command(about = "Get a single project by ID")]
22 Get {
23 #[arg(help = "Project ID (P-<uuid> format)")]
24 id: String,
25 },
26 #[command(about = "Create a new project")]
27 Create {
28 #[arg(long, help = "Project title (required)")]
29 title: String,
30 #[arg(long, help = "Project description/notes")]
31 note: Option<String>,
32 #[arg(long, help = "Parent project ID (P-<uuid>) for nesting")]
33 parent: Option<String>,
34 #[arg(long, help = "Color hex code (e.g. #FF0000)")]
35 color: Option<String>,
36 #[arg(long, help = "Emoji icon for the project")]
37 emoji: Option<String>,
38 #[arg(long, help = "Start date (ISO 8601 format)")]
39 start: Option<String>,
40 #[arg(long, help = "End date (ISO 8601 format)")]
41 end: Option<String>,
42 #[arg(long, help = "Create as a notebook instead of a project")]
43 notebook: bool,
44 },
45 #[command(about = "Update an existing project (only specified fields are changed)")]
46 Update {
47 #[arg(help = "Project ID to update (P-<uuid> format)")]
48 id: String,
49 #[arg(long, help = "New project title")]
50 title: Option<String>,
51 #[arg(long, help = "New project description/notes")]
52 note: Option<String>,
53 #[arg(long, help = "New parent project ID (P-<uuid>)")]
54 parent: Option<String>,
55 #[arg(long, help = "New color hex code")]
56 color: Option<String>,
57 #[arg(long, help = "New emoji icon")]
58 emoji: Option<String>,
59 #[arg(long, help = "New start date (ISO 8601)")]
60 start: Option<String>,
61 #[arg(long, help = "New end date (ISO 8601)")]
62 end: Option<String>,
63 #[arg(long, help = "Set notebook flag (true/false)")]
64 notebook: Option<bool>,
65 },
66 #[command(about = "Delete a project by ID (soft-delete)")]
67 Delete {
68 #[arg(help = "Project ID to delete (P-<uuid> format)")]
69 id: String,
70 },
71}
72
73pub fn run(client: &ApiClient, cmd: ProjectCmd, json: bool, tz: Option<Tz>) -> Result<()> {
74 match cmd {
75 ProjectCmd::List {
76 max_count,
77 offset,
78 include_removed,
79 include_archived,
80 } => {
81 let mut query: Vec<(&str, String)> = Vec::new();
82 if let Some(v) = max_count {
83 query.push(("maxCount", v.to_string()));
84 }
85 if let Some(v) = offset {
86 query.push(("offset", v.to_string()));
87 }
88 if include_removed {
89 query.push(("includeRemoved", "true".to_string()));
90 }
91 if include_archived {
92 query.push(("includeArchived", "true".to_string()));
93 }
94
95 if json {
96 let resp: serde_json::Value = client.get("/v2/project", &query)?;
97 println!("{}", serde_json::to_string_pretty(&resp)?);
98 } else {
99 let resp: ProjectListResponse = client.get("/v2/project", &query)?;
100 if resp.projects.is_empty() {
101 println!("No projects found.");
102 } else {
103 for p in &resp.projects {
104 println!("{}\n", p.display_list_item());
105 }
106 }
107 }
108 }
109 ProjectCmd::Get { id } => {
110 if json {
111 let resp: serde_json::Value = client.get(&format!("/v2/project/{}", id), &[])?;
112 println!("{}", serde_json::to_string_pretty(&resp)?);
113 } else {
114 let project: Project = client.get(&format!("/v2/project/{}", id), &[])?;
115 println!("{}", project.display_detail(tz));
116 }
117 }
118 ProjectCmd::Create {
119 title,
120 note,
121 parent,
122 color,
123 emoji,
124 start,
125 end,
126 notebook,
127 } => {
128 let data = ProjectCreate {
129 title,
130 note,
131 parent,
132 color,
133 emoji,
134 start,
135 end,
136 is_notebook: if notebook { Some(true) } else { None },
137 };
138 if json {
139 let resp: serde_json::Value = client.post("/v2/project", &data)?;
140 println!("{}", serde_json::to_string_pretty(&resp)?);
141 } else {
142 let project: Project = client.post("/v2/project", &data)?;
143 println!("Created project {}", project.id);
144 }
145 }
146 ProjectCmd::Update {
147 id,
148 title,
149 note,
150 parent,
151 color,
152 emoji,
153 start,
154 end,
155 notebook,
156 } => {
157 let data = ProjectUpdate {
158 title,
159 note,
160 parent,
161 color,
162 emoji,
163 start,
164 end,
165 is_notebook: notebook,
166 };
167 if json {
168 let resp: serde_json::Value =
169 client.patch(&format!("/v2/project/{}", id), &data)?;
170 println!("{}", serde_json::to_string_pretty(&resp)?);
171 } else {
172 let project: Project = client.patch(&format!("/v2/project/{}", id), &data)?;
173 println!("Updated project {}", project.id);
174 }
175 }
176 ProjectCmd::Delete { id } => {
177 client.delete(&format!("/v2/project/{}", id))?;
178 println!("Deleted project {}", id);
179 }
180 }
181 Ok(())
182}