Skip to main content

raps_cli/commands/config/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! Configuration management commands
5//!
6//! Commands for managing profiles and configuration settings.
7
8mod config_ops;
9mod context;
10mod profiles;
11
12use anyhow::{Context, Result};
13use clap::Subcommand;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::path::PathBuf;
17
18use crate::output::OutputFormat;
19
20#[derive(Debug, Subcommand)]
21pub enum ConfigCommands {
22    /// Manage profiles (create, list, use, delete)
23    #[command(subcommand)]
24    Profile(ProfileCommands),
25
26    /// Get a configuration value
27    Get {
28        /// Configuration key (e.g., client_id, base_url)
29        key: String,
30    },
31
32    /// Set a configuration value for the active profile
33    Set {
34        /// Configuration key (e.g., client_id, base_url)
35        key: String,
36        /// Configuration value
37        value: String,
38    },
39
40    /// Set or show the current working context (hub, project, account)
41    #[command(subcommand)]
42    Context(ContextCommands),
43
44    /// Migrate tokens from plaintext file to OS keychain
45    MigrateTokens,
46}
47
48#[derive(Debug, Subcommand)]
49pub enum ContextCommands {
50    /// Show current context settings
51    Show,
52
53    /// Set context value
54    Set {
55        /// Key to set (hub_id, project_id, account_id)
56        key: String,
57        /// Value to set (use "clear" to remove)
58        value: String,
59    },
60
61    /// Clear all context values
62    Clear,
63}
64
65#[derive(Debug, Subcommand)]
66pub enum ProfileCommands {
67    /// Create a new profile
68    Create {
69        /// Profile name
70        name: String,
71    },
72
73    /// List all profiles
74    List,
75
76    /// Set the active profile
77    Use {
78        /// Profile name
79        name: String,
80    },
81
82    /// Delete a profile
83    Delete {
84        /// Profile name
85        name: String,
86    },
87
88    /// Show current active profile
89    Current,
90
91    /// Export profiles to a file
92    Export {
93        /// Output file path
94        #[arg(long = "out-file", default_value = "profiles-export.json")]
95        out_file: std::path::PathBuf,
96
97        /// Include secrets (client_id, client_secret) - use with caution
98        #[arg(long)]
99        include_secrets: bool,
100
101        /// Export specific profile (default: all)
102        #[arg(short, long)]
103        name: Option<String>,
104    },
105
106    /// Import profiles from a file
107    Import {
108        /// Input file path
109        file: std::path::PathBuf,
110
111        /// Overwrite existing profiles with same name
112        #[arg(long)]
113        overwrite: bool,
114    },
115
116    /// Compare two profiles
117    Diff {
118        /// First profile name
119        profile1: String,
120        /// Second profile name
121        profile2: String,
122    },
123}
124
125impl ConfigCommands {
126    pub async fn execute(self, output_format: OutputFormat) -> Result<()> {
127        match self {
128            ConfigCommands::Profile(cmd) => cmd.execute(output_format).await,
129            ConfigCommands::Get { key } => config_ops::get_config(&key, output_format).await,
130            ConfigCommands::Set { key, value } => {
131                config_ops::set_config(&key, &value, output_format).await
132            }
133            ConfigCommands::Context(cmd) => cmd.execute(output_format).await,
134            ConfigCommands::MigrateTokens => {
135                raps_kernel::storage::TokenStorage::migrate_to_keychain()
136            }
137        }
138    }
139}
140
141impl ContextCommands {
142    pub async fn execute(self, output_format: OutputFormat) -> Result<()> {
143        match self {
144            ContextCommands::Show => context::show_context(output_format).await,
145            ContextCommands::Set { key, value } => {
146                context::set_context(&key, &value, output_format).await
147            }
148            ContextCommands::Clear => context::clear_context(output_format).await,
149        }
150    }
151}
152
153impl ProfileCommands {
154    pub async fn execute(self, output_format: OutputFormat) -> Result<()> {
155        match self {
156            ProfileCommands::Create { name } => {
157                profiles::create_profile(&name, output_format).await
158            }
159            ProfileCommands::List => profiles::list_profiles(output_format).await,
160            ProfileCommands::Use { name } => profiles::use_profile(&name, output_format).await,
161            ProfileCommands::Delete { name } => {
162                profiles::delete_profile(&name, output_format).await
163            }
164            ProfileCommands::Current => profiles::show_current_profile(output_format).await,
165            ProfileCommands::Export {
166                out_file,
167                include_secrets,
168                name,
169            } => profiles::export_profiles(&out_file, include_secrets, name, output_format).await,
170            ProfileCommands::Import { file, overwrite } => {
171                profiles::import_profiles(&file, overwrite, output_format).await
172            }
173            ProfileCommands::Diff { profile1, profile2 } => {
174                profiles::diff_profiles(&profile1, &profile2, output_format).await
175            }
176        }
177    }
178}
179
180/// Profile configuration structure
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ProfileConfig {
183    pub client_id: Option<String>,
184    pub client_secret: Option<String>,
185    pub base_url: Option<String>,
186    pub callback_url: Option<String>,
187    pub da_nickname: Option<String>,
188    pub use_keychain: Option<bool>,
189    /// Sticky context: default hub ID for commands that need it
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub context_hub_id: Option<String>,
192    /// Sticky context: default project ID for commands that need it
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub context_project_id: Option<String>,
195    /// Sticky context: default account ID for admin commands
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub context_account_id: Option<String>,
198}
199
200/// Profiles storage structure
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub(crate) struct ProfilesData {
203    pub active_profile: Option<String>,
204    pub profiles: HashMap<String, ProfileConfig>,
205}
206
207fn profiles_path() -> Result<PathBuf> {
208    let proj_dirs = directories::ProjectDirs::from("com", "autodesk", "raps")
209        .context("Failed to get project directories")?;
210    let config_dir = proj_dirs.config_dir();
211    std::fs::create_dir_all(config_dir)?;
212    Ok(config_dir.join("profiles.json"))
213}
214
215pub(crate) fn load_profiles() -> Result<ProfilesData> {
216    let path = profiles_path()?;
217    if !path.exists() {
218        return Ok(ProfilesData {
219            active_profile: None,
220            profiles: HashMap::new(),
221        });
222    }
223
224    let content = std::fs::read_to_string(&path)?;
225    let data: ProfilesData =
226        serde_json::from_str(&content).context("Failed to parse profiles.json")?;
227    Ok(data)
228}
229
230fn save_profiles(data: &ProfilesData) -> Result<()> {
231    let path = profiles_path()?;
232    let content = serde_json::to_string_pretty(data)?;
233    std::fs::write(&path, content)?;
234    Ok(())
235}