Skip to main content

clickup_cli/commands/
setup.rs

1use clap::Args;
2use crate::client::ClickUpClient;
3use crate::config::{AuthConfig, Config, DefaultsConfig};
4use crate::error::CliError;
5use crate::Cli;
6use std::io::{self, Write};
7
8#[derive(Args)]
9pub struct SetupArgs {
10    /// API token (skip interactive prompt)
11    #[arg(long)]
12    pub token: Option<String>,
13}
14
15pub async fn execute(args: SetupArgs, cli: &Cli) -> Result<(), CliError> {
16    let token = match args.token.or_else(|| cli.token.clone()) {
17        Some(t) => t,
18        None => prompt_token()?,
19    };
20
21    // Validate token by hitting /v2/user
22    let client = ClickUpClient::new(&token, cli.timeout)?;
23    let user_resp = client.get("/v2/user").await?;
24    let username = user_resp
25        .get("user")
26        .and_then(|u| u.get("username"))
27        .and_then(|u| u.as_str())
28        .unwrap_or("Unknown");
29    eprintln!("Validating... ✓ Authenticated as {}", username);
30
31    // Fetch workspaces
32    let teams_resp = client.get("/v2/team").await?;
33    let teams = teams_resp
34        .get("teams")
35        .and_then(|t| t.as_array())
36        .cloned()
37        .unwrap_or_default();
38
39    let workspace_id = match teams.len() {
40        0 => {
41            return Err(CliError::ClientError {
42                message: "No workspaces found for this token".into(),
43                status: 0,
44            });
45        }
46        1 => {
47            let ws = &teams[0];
48            let id = ws.get("id").and_then(|v| v.as_str()).unwrap_or("");
49            let name = ws.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown");
50            eprintln!("\nOnly one workspace found — setting as default.");
51            eprintln!("  {} (ID: {})", name, id);
52            id.to_string()
53        }
54        _ => {
55            eprintln!("\nFetching workspaces...");
56            for (i, ws) in teams.iter().enumerate() {
57                let id = ws.get("id").and_then(|v| v.as_str()).unwrap_or("");
58                let name = ws.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown");
59                eprintln!("  [{}] {} (ID: {})", i + 1, name, id);
60            }
61            let choice = prompt_choice(teams.len())?;
62            let ws = &teams[choice - 1];
63            ws.get("id")
64                .and_then(|v| v.as_str())
65                .unwrap_or("")
66                .to_string()
67        }
68    };
69
70    let config = Config {
71        auth: AuthConfig {
72            token: token.clone(),
73        },
74        defaults: DefaultsConfig {
75            workspace_id: Some(workspace_id),
76            output: None,
77        },
78    };
79    config.save()?;
80
81    let path = Config::config_path()?;
82    eprintln!("Config saved to {}", path.display());
83    Ok(())
84}
85
86fn prompt_token() -> Result<String, CliError> {
87    eprint!("API Token (get one at Settings > Apps): ");
88    io::stderr().flush()?;
89    let mut token = String::new();
90    io::stdin().read_line(&mut token)?;
91    let token = token.trim().to_string();
92    if token.is_empty() {
93        return Err(CliError::ConfigError("No token provided".into()));
94    }
95    Ok(token)
96}
97
98fn prompt_choice(max: usize) -> Result<usize, CliError> {
99    eprint!("\nSelect workspace [1-{}]: ", max);
100    io::stderr().flush()?;
101    let mut input = String::new();
102    io::stdin().read_line(&mut input)?;
103    let choice: usize = input
104        .trim()
105        .parse()
106        .map_err(|_| CliError::ConfigError("Invalid selection".into()))?;
107    if choice < 1 || choice > max {
108        return Err(CliError::ConfigError(format!("Selection must be between 1 and {}", max)));
109    }
110    Ok(choice)
111}