Skip to main content

clickup_cli/commands/
workspace.rs

1use clap::Subcommand;
2use crate::client::ClickUpClient;
3use crate::commands::auth::resolve_token;
4use crate::config::Config;
5use crate::error::CliError;
6use crate::output::OutputConfig;
7use crate::Cli;
8
9#[derive(Subcommand)]
10pub enum WorkspaceCommands {
11    /// List workspaces
12    List,
13    /// Show seat usage
14    Seats,
15    /// Show current plan
16    Plan,
17}
18
19pub fn resolve_workspace(cli: &Cli) -> Result<String, CliError> {
20    // 1. --workspace flag
21    if let Some(ws) = &cli.workspace {
22        return Ok(ws.clone());
23    }
24    // 2. CLICKUP_WORKSPACE env var
25    if let Ok(ws) = std::env::var("CLICKUP_WORKSPACE") {
26        if !ws.is_empty() {
27            return Ok(ws);
28        }
29    }
30    // 3. Config file
31    let config = Config::load()?;
32    config
33        .defaults
34        .workspace_id
35        .ok_or_else(|| CliError::ConfigError("No default workspace. Use --workspace, CLICKUP_WORKSPACE, or run 'clickup setup'".into()))
36}
37
38pub async fn execute(command: WorkspaceCommands, cli: &Cli) -> Result<(), CliError> {
39    let token = resolve_token(cli)?;
40    let client = ClickUpClient::new(&token, cli.timeout)?;
41    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
42
43    match command {
44        WorkspaceCommands::List => {
45            let resp = client.get("/v2/team").await?;
46            let teams = resp
47                .get("teams")
48                .and_then(|t| t.as_array())
49                .cloned()
50                .unwrap_or_default();
51            // Simplify for table output — extract id, name, member count
52            let items: Vec<serde_json::Value> = teams
53                .iter()
54                .map(|ws| {
55                    serde_json::json!({
56                        "id": ws.get("id").and_then(|v| v.as_str()).unwrap_or("-"),
57                        "name": ws.get("name").and_then(|v| v.as_str()).unwrap_or("-"),
58                        "members": ws.get("members").and_then(|m| m.as_array()).map(|a| a.len()).unwrap_or(0),
59                    })
60                })
61                .collect();
62            output.print_items(&items, &["id", "name", "members"], "id");
63            Ok(())
64        }
65        WorkspaceCommands::Seats => {
66            let ws_id = resolve_workspace(cli)?;
67            let resp = client.get(&format!("/v2/team/{}/seats", ws_id)).await?;
68            if cli.output == "json" {
69                println!("{}", serde_json::to_string_pretty(&resp).unwrap());
70            } else {
71                // seats response has filled_members_seats, empty_members_seats, etc.
72                let filled = resp.get("seats").and_then(|s| s.get("members"))
73                    .and_then(|m| m.get("filled_members_seats"))
74                    .and_then(|v| v.as_u64())
75                    .unwrap_or(0);
76                let total = resp.get("seats").and_then(|s| s.get("members"))
77                    .and_then(|m| m.get("total_members_seats"))
78                    .and_then(|v| v.as_u64())
79                    .or_else(|| {
80                        // Try alternative: top-level fields
81                        resp.get("filled_members_seats").and_then(|v| v.as_u64())
82                    })
83                    .unwrap_or(0);
84                let items = vec![serde_json::json!({
85                    "filled_seats": filled,
86                    "total_seats": total,
87                })];
88                output.print_items(&items, &["filled_seats", "total_seats"], "filled_seats");
89            }
90            Ok(())
91        }
92        WorkspaceCommands::Plan => {
93            let ws_id = resolve_workspace(cli)?;
94            let resp = client.get(&format!("/v2/team/{}/plan", ws_id)).await?;
95            if cli.output == "json" {
96                println!("{}", serde_json::to_string_pretty(&resp).unwrap());
97            } else {
98                let plan_name = resp.get("plan_id")
99                    .or_else(|| resp.get("plan_name"))
100                    .and_then(|v| v.as_str())
101                    .unwrap_or("-");
102                println!("Plan: {}", plan_name);
103            }
104            Ok(())
105        }
106    }
107}