Skip to main content

raps_cli/commands/
project.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! Project management commands
5//!
6//! Commands for listing and viewing projects (requires 3-legged auth).
7
8use anyhow::{Context, Result};
9use clap::Subcommand;
10use colored::Colorize;
11#[allow(unused_imports)]
12use raps_kernel::prompts;
13use serde::Serialize;
14
15use crate::commands::interactive;
16use crate::commands::tracked::tracked_op;
17use crate::output::OutputFormat;
18use raps_dm::DataManagementClient;
19// use raps_kernel::output::OutputFormat;
20
21#[derive(Debug, Subcommand)]
22pub enum ProjectCommands {
23    /// List projects in a hub
24    List {
25        /// Hub ID (interactive if not provided)
26        hub_id: Option<String>,
27    },
28
29    /// Get project details
30    Info {
31        /// Hub ID (interactive if not provided)
32        hub_id: Option<String>,
33        /// Project ID (interactive if not provided)
34        project_id: Option<String>,
35    },
36}
37
38impl ProjectCommands {
39    pub async fn execute(
40        self,
41        client: &DataManagementClient,
42        output_format: OutputFormat,
43    ) -> Result<()> {
44        match self {
45            ProjectCommands::List { hub_id } => list_projects(client, hub_id, output_format).await,
46            ProjectCommands::Info { hub_id, project_id } => {
47                project_info(client, &hub_id, &project_id, output_format).await
48            }
49        }
50    }
51}
52
53#[derive(Serialize)]
54struct ProjectListOutput {
55    id: String,
56    name: String,
57    project_type: String,
58    scopes: Option<Vec<String>>,
59}
60
61async fn list_projects(
62    client: &DataManagementClient,
63    hub_id: Option<String>,
64    output_format: OutputFormat,
65) -> Result<()> {
66    // Get hub ID interactively if not provided
67    let hub = match hub_id {
68        Some(h) => h,
69        None => interactive::prompt_for_hub(client).await?,
70    };
71
72    let projects = tracked_op("Fetching projects", output_format, || async {
73        client.list_projects(&hub).await.context(format!(
74            "Failed to list projects in hub '{}'. Verify the hub ID and your permissions",
75            hub
76        ))
77    })
78    .await?;
79
80    let project_outputs: Vec<ProjectListOutput> = projects
81        .iter()
82        .map(|p| ProjectListOutput {
83            id: p.id.clone(),
84            name: p.attributes.name.clone(),
85            project_type: p.project_type.clone(),
86            scopes: p.attributes.scopes.clone(),
87        })
88        .collect();
89
90    if project_outputs.is_empty() {
91        match output_format {
92            OutputFormat::Table => println!("{}", "No projects found in this hub.".yellow()),
93            _ => {
94                output_format.write(&Vec::<ProjectListOutput>::new())?;
95            }
96        }
97        return Ok(());
98    }
99
100    match output_format {
101        OutputFormat::Table => {
102            println!("\n{}", "Projects:".bold());
103            println!("{}", "-".repeat(80));
104
105            for project in &project_outputs {
106                println!("  {} {}", "-".cyan(), project.name.bold());
107                println!("    {} {}", "ID:".dimmed(), project.id);
108                if let Some(ref scopes) = project.scopes {
109                    println!("    {} {:?}", "Scopes:".dimmed(), scopes);
110                }
111            }
112
113            println!("{}", "-".repeat(80));
114            println!(
115                "\n{}",
116                "Use 'raps folder list <hub-id> <project-id>' to see folders".dimmed()
117            );
118        }
119        _ => {
120            output_format.write(&project_outputs)?;
121        }
122    }
123    Ok(())
124}
125
126#[derive(Serialize)]
127struct ProjectInfoOutput {
128    id: String,
129    name: String,
130    project_type: String,
131    scopes: Option<Vec<String>>,
132    top_folders: Vec<FolderOutput>,
133}
134
135#[derive(Serialize)]
136struct FolderOutput {
137    id: String,
138    name: String,
139    display_name: Option<String>,
140}
141
142async fn project_info(
143    client: &DataManagementClient,
144    opt_hub_id: &Option<String>,
145    opt_project_id: &Option<String>,
146    output_format: OutputFormat,
147) -> Result<()> {
148    let hub_id = match opt_hub_id {
149        Some(h) => h.clone(),
150        None => interactive::prompt_for_hub(client).await?,
151    };
152
153    let project_id = match opt_project_id {
154        Some(p) => p.clone(),
155        None => interactive::prompt_for_project(client, &hub_id).await?,
156    };
157    let (project, folders) = tracked_op("Fetching project details", output_format, || async {
158        let project = client
159            .get_project(&hub_id, &project_id)
160            .await
161            .context(format!(
162                "Failed to get project '{}'. Verify the project ID and your permissions",
163                project_id
164            ))?;
165        let folders = client
166            .get_top_folders(&hub_id, &project_id)
167            .await
168            .context(format!(
169                "Failed to get top folders for project '{}'. You may lack folder-level permissions",
170                project_id
171            ))?;
172        Ok((project, folders))
173    })
174    .await?;
175
176    let folder_outputs: Vec<FolderOutput> = folders
177        .iter()
178        .map(|f| FolderOutput {
179            id: f.id.clone(),
180            name: f.attributes.name.clone(),
181            display_name: f.attributes.display_name.clone(),
182        })
183        .collect();
184
185    let output = ProjectInfoOutput {
186        id: project.id.clone(),
187        name: project.attributes.name.clone(),
188        project_type: project.project_type.clone(),
189        scopes: project.attributes.scopes.clone(),
190        top_folders: folder_outputs,
191    };
192
193    match output_format {
194        OutputFormat::Table => {
195            println!("\n{}", "Project Details".bold());
196            println!("{}", "-".repeat(60));
197            println!("  {} {}", "Name:".bold(), output.name.cyan());
198            println!("  {} {}", "ID:".bold(), output.id);
199            println!("  {} {}", "Type:".bold(), output.project_type);
200
201            if let Some(ref scopes) = output.scopes {
202                println!("  {} {:?}", "Scopes:".bold(), scopes);
203            }
204
205            println!("\n{}", "Top Folders:".bold());
206            for folder in &output.top_folders {
207                println!(
208                    "  {} {} ({})",
209                    "[folder]".dimmed(),
210                    folder.display_name.as_ref().unwrap_or(&folder.name),
211                    folder.id.dimmed()
212                );
213            }
214
215            println!("{}", "-".repeat(60));
216        }
217        _ => {
218            output_format.write(&output)?;
219        }
220    }
221    Ok(())
222}