singularity_cli/commands/
project.rs1use anyhow::Result;
2use clap::Subcommand;
3use tabled::Table;
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) -> 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 println!("{}", Table::new(&resp.projects));
104 }
105 }
106 }
107 ProjectCmd::Get { id } => {
108 if json {
109 let resp: serde_json::Value = client.get(&format!("/v2/project/{}", id), &[])?;
110 println!("{}", serde_json::to_string_pretty(&resp)?);
111 } else {
112 let project: Project = client.get(&format!("/v2/project/{}", id), &[])?;
113 project.display_detail();
114 }
115 }
116 ProjectCmd::Create {
117 title,
118 note,
119 parent,
120 color,
121 emoji,
122 start,
123 end,
124 notebook,
125 } => {
126 let data = ProjectCreate {
127 title,
128 note,
129 parent,
130 color,
131 emoji,
132 start,
133 end,
134 is_notebook: if notebook { Some(true) } else { None },
135 };
136 if json {
137 let resp: serde_json::Value = client.post("/v2/project", &data)?;
138 println!("{}", serde_json::to_string_pretty(&resp)?);
139 } else {
140 let project: Project = client.post("/v2/project", &data)?;
141 println!("Created project {}", project.id);
142 }
143 }
144 ProjectCmd::Update {
145 id,
146 title,
147 note,
148 parent,
149 color,
150 emoji,
151 start,
152 end,
153 notebook,
154 } => {
155 let data = ProjectUpdate {
156 title,
157 note,
158 parent,
159 color,
160 emoji,
161 start,
162 end,
163 is_notebook: notebook,
164 };
165 if json {
166 let resp: serde_json::Value =
167 client.patch(&format!("/v2/project/{}", id), &data)?;
168 println!("{}", serde_json::to_string_pretty(&resp)?);
169 } else {
170 let project: Project = client.patch(&format!("/v2/project/{}", id), &data)?;
171 println!("Updated project {}", project.id);
172 }
173 }
174 ProjectCmd::Delete { id } => {
175 client.delete(&format!("/v2/project/{}", id))?;
176 println!("Deleted project {}", id);
177 }
178 }
179 Ok(())
180}