Skip to main content

clickup_cli/commands/
setup.rs

1use crate::client::ClickUpClient;
2use crate::config::{AuthConfig, Config, DefaultsConfig};
3use crate::error::CliError;
4use crate::Cli;
5use clap::Args;
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        git: Default::default(),
79    };
80    config.save()?;
81
82    let path = Config::config_path()?;
83    eprintln!("Config saved to {}", path.display());
84    Ok(())
85}
86
87fn prompt_token() -> Result<String, CliError> {
88    eprint!("API Token (get one at Settings > Apps): ");
89    io::stderr().flush()?;
90    let mut token = String::new();
91    io::stdin().read_line(&mut token)?;
92    let token = token.trim().to_string();
93    if token.is_empty() {
94        return Err(CliError::ConfigError("No token provided".into()));
95    }
96    Ok(token)
97}
98
99fn prompt_choice(max: usize) -> Result<usize, CliError> {
100    eprint!("\nSelect workspace [1-{}]: ", max);
101    io::stderr().flush()?;
102    let mut input = String::new();
103    io::stdin().read_line(&mut input)?;
104    let choice: usize = input
105        .trim()
106        .parse()
107        .map_err(|_| CliError::ConfigError("Invalid selection".into()))?;
108    if choice < 1 || choice > max {
109        return Err(CliError::ConfigError(format!(
110            "Selection must be between 1 and {}",
111            max
112        )));
113    }
114    Ok(choice)
115}