Skip to main content

clickup_cli/commands/
workspace.rs

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