Skip to main content

clickup_cli/commands/
goal.rs

1use crate::client::ClickUpClient;
2use crate::commands::auth::resolve_token;
3use crate::commands::workspace::resolve_workspace;
4use crate::error::CliError;
5use crate::output::OutputConfig;
6use crate::Cli;
7use clap::Subcommand;
8
9#[derive(Subcommand)]
10pub enum GoalCommands {
11    /// List goals in workspace
12    List {
13        /// Include completed goals
14        #[arg(long)]
15        include_completed: bool,
16    },
17    /// Get a goal by ID
18    Get {
19        /// Goal ID
20        id: String,
21    },
22    /// Create a goal
23    Create {
24        /// Goal name
25        #[arg(long)]
26        name: String,
27        /// Due date (Unix ms)
28        #[arg(long)]
29        due_date: String,
30        /// Description
31        #[arg(long)]
32        description: String,
33        /// Color hex (e.g. #32a852)
34        #[arg(long)]
35        color: Option<String>,
36        /// Owner user ID
37        #[arg(long)]
38        owner: Option<String>,
39    },
40    /// Update a goal
41    Update {
42        /// Goal ID
43        id: String,
44        /// New name
45        #[arg(long)]
46        name: Option<String>,
47        /// New due date (Unix ms)
48        #[arg(long)]
49        due_date: Option<String>,
50        /// New description
51        #[arg(long)]
52        description: Option<String>,
53        /// New color hex
54        #[arg(long)]
55        color: Option<String>,
56        /// Add owner by user ID
57        #[arg(long)]
58        add_owner: Option<String>,
59        /// Remove owner by user ID
60        #[arg(long)]
61        rem_owner: Option<String>,
62    },
63    /// Delete a goal
64    Delete {
65        /// Goal ID
66        id: String,
67    },
68    /// Add a key result to a goal
69    AddKr {
70        /// Goal ID
71        goal_id: String,
72        /// Key result name
73        #[arg(long)]
74        name: String,
75        /// Type: number, currency, boolean, percentage, automatic
76        #[arg(long, name = "type")]
77        kr_type: String,
78        /// Starting step value
79        #[arg(long)]
80        steps_start: i64,
81        /// Target step value
82        #[arg(long)]
83        steps_end: i64,
84        /// Unit label (e.g. "tasks")
85        #[arg(long)]
86        unit: Option<String>,
87        /// Owner user ID
88        #[arg(long)]
89        owner: Option<String>,
90    },
91    /// Update a key result
92    UpdateKr {
93        /// Key result ID
94        kr_id: String,
95        /// Current step value
96        #[arg(long)]
97        steps_current: i64,
98        /// Note
99        #[arg(long)]
100        note: Option<String>,
101    },
102    /// Delete a key result
103    DeleteKr {
104        /// Key result ID
105        kr_id: String,
106    },
107}
108
109const GOAL_FIELDS: &[&str] = &["id", "name", "percent_completed", "due_date"];
110
111pub async fn execute(command: GoalCommands, cli: &Cli) -> Result<(), CliError> {
112    let token = resolve_token(cli)?;
113    let client = ClickUpClient::new(&token, cli.timeout)?;
114    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
115
116    match command {
117        GoalCommands::List { include_completed } => {
118            let ws_id = resolve_workspace(cli)?;
119            let resp = client
120                .get(&format!(
121                    "/v2/team/{}/goal?include_completed={}",
122                    ws_id, include_completed
123                ))
124                .await?;
125            let goals = resp
126                .get("goals")
127                .and_then(|g| g.as_array())
128                .cloned()
129                .unwrap_or_default();
130            output.print_items(&goals, GOAL_FIELDS, "id");
131            Ok(())
132        }
133        GoalCommands::Get { id } => {
134            let resp = client.get(&format!("/v2/goal/{}", id)).await?;
135            let goal = resp.get("goal").cloned().unwrap_or(resp);
136            output.print_single(&goal, GOAL_FIELDS, "id");
137            Ok(())
138        }
139        GoalCommands::Create {
140            name,
141            due_date,
142            description,
143            color,
144            owner,
145        } => {
146            let ws_id = resolve_workspace(cli)?;
147            // ClickUp's create-goal spec requires `multiple_owners` (bool).
148            // The CLI exposes only a single `--owner`, so we always send false;
149            // multi-owner goals can be created via the MCP tool or a raw API call.
150            let mut body = serde_json::json!({
151                "name": name,
152                "due_date": due_date,
153                "description": description,
154                "multiple_owners": false,
155            });
156            if let Some(c) = color {
157                body["color"] = serde_json::Value::String(c);
158            }
159            if let Some(o) = owner {
160                body["owners"] = serde_json::json!([o]);
161            }
162            let resp = client
163                .post(&format!("/v2/team/{}/goal", ws_id), &body)
164                .await?;
165            let goal = resp.get("goal").cloned().unwrap_or(resp);
166            output.print_single(&goal, GOAL_FIELDS, "id");
167            Ok(())
168        }
169        GoalCommands::Update {
170            id,
171            name,
172            due_date,
173            description,
174            color,
175            add_owner,
176            rem_owner,
177        } => {
178            let mut body = serde_json::Map::new();
179            if let Some(n) = name {
180                body.insert("name".into(), serde_json::Value::String(n));
181            }
182            if let Some(d) = due_date {
183                body.insert("due_date".into(), serde_json::Value::String(d));
184            }
185            if let Some(d) = description {
186                body.insert("description".into(), serde_json::Value::String(d));
187            }
188            if let Some(c) = color {
189                body.insert("color".into(), serde_json::Value::String(c));
190            }
191            if let Some(o) = add_owner {
192                body.insert("add_owners".into(), serde_json::json!([o]));
193            }
194            if let Some(o) = rem_owner {
195                body.insert("rem_owners".into(), serde_json::json!([o]));
196            }
197            let resp = client
198                .put(
199                    &format!("/v2/goal/{}", id),
200                    &serde_json::Value::Object(body),
201                )
202                .await?;
203            let goal = resp.get("goal").cloned().unwrap_or(resp);
204            output.print_single(&goal, GOAL_FIELDS, "id");
205            Ok(())
206        }
207        GoalCommands::Delete { id } => {
208            client.delete(&format!("/v2/goal/{}", id)).await?;
209            output.print_message(&format!("Goal {} deleted", id));
210            Ok(())
211        }
212        GoalCommands::AddKr {
213            goal_id,
214            name,
215            kr_type,
216            steps_start,
217            steps_end,
218            unit,
219            owner,
220        } => {
221            let mut body = serde_json::json!({
222                "name": name,
223                "type": kr_type,
224                "steps_start": steps_start,
225                "steps_end": steps_end,
226            });
227            if let Some(u) = unit {
228                body["unit"] = serde_json::Value::String(u);
229            }
230            if let Some(o) = owner {
231                body["owners"] = serde_json::json!([o]);
232            }
233            let resp = client
234                .post(&format!("/v2/goal/{}/key_result", goal_id), &body)
235                .await?;
236            let kr = resp.get("key_result").cloned().unwrap_or(resp);
237            output.print_single(
238                &kr,
239                &["id", "name", "type", "steps_start", "steps_end"],
240                "id",
241            );
242            Ok(())
243        }
244        GoalCommands::UpdateKr {
245            kr_id,
246            steps_current,
247            note,
248        } => {
249            let mut body = serde_json::json!({ "steps_current": steps_current });
250            if let Some(n) = note {
251                body["note"] = serde_json::Value::String(n);
252            }
253            let resp = client
254                .put(&format!("/v2/key_result/{}", kr_id), &body)
255                .await?;
256            let kr = resp.get("key_result").cloned().unwrap_or(resp);
257            output.print_single(&kr, &["id", "name", "steps_current", "steps_end"], "id");
258            Ok(())
259        }
260        GoalCommands::DeleteKr { kr_id } => {
261            client.delete(&format!("/v2/key_result/{}", kr_id)).await?;
262            output.print_message(&format!("Key result {} deleted", kr_id));
263            Ok(())
264        }
265    }
266}