Skip to main content

sqlite_graphrag/commands/
config_cmd.rs

1use crate::config::{self, compute_fingerprint, mask_key, ApiKeyEntry};
2use crate::errors::AppError;
3use clap::{Args, Subcommand};
4use serde_json::json;
5use std::io::{self, Read};
6
7#[derive(Debug, Args)]
8pub struct ConfigArgs {
9    #[command(subcommand)]
10    pub action: ConfigAction,
11}
12
13#[derive(Debug, Subcommand)]
14pub enum ConfigAction {
15    /// Add an API key for a provider (reads from stdin to avoid shell history).
16    AddKey {
17        #[arg(long)]
18        provider: String,
19        #[arg(long, default_value_t = true)]
20        from_stdin: bool,
21    },
22    /// List stored API keys (masked) with fingerprints.
23    ListKeys,
24    /// Remove an API key by its fingerprint.
25    RemoveKey { fingerprint: String },
26    /// Diagnose which layer won for each provider (env/config/cli).
27    Doctor,
28    /// Print the resolved XDG config file path.
29    Path,
30}
31
32pub fn run(args: ConfigArgs) -> Result<(), AppError> {
33    match args.action {
34        ConfigAction::AddKey {
35            provider,
36            from_stdin,
37        } => {
38            let key = if from_stdin {
39                let mut buf = String::new();
40                io::stdin().read_to_string(&mut buf).map_err(AppError::Io)?;
41                buf.trim().to_string()
42            } else {
43                return Err(AppError::Validation(
44                    "--from-stdin is required to avoid shell history exposure".into(),
45                ));
46            };
47            if key.is_empty() {
48                return Err(AppError::Validation("API key cannot be empty".into()));
49            }
50            let fingerprint = compute_fingerprint(&key);
51            let entry = ApiKeyEntry {
52                provider: provider.clone(),
53                value: key,
54                added_at: chrono::Utc::now().to_rfc3339(),
55                fingerprint: fingerprint.clone(),
56            };
57            let mut cfg = config::load_config()?;
58            cfg.keys.retain(|k| k.provider != provider);
59            cfg.keys.push(entry);
60            config::save_config(&cfg)?;
61            let output = json!({
62                "action": "key_added",
63                "provider": provider,
64                "fingerprint": fingerprint,
65            });
66            println!("{}", serde_json::to_string(&output).unwrap());
67            Ok(())
68        }
69        ConfigAction::ListKeys => {
70            let cfg = config::load_config()?;
71            let keys: Vec<_> = cfg
72                .keys
73                .iter()
74                .map(|k| {
75                    json!({
76                        "provider": k.provider,
77                        "fingerprint": k.fingerprint,
78                        "masked_value": mask_key(&k.value),
79                        "added_at": k.added_at,
80                    })
81                })
82                .collect();
83            let output = json!({ "keys": keys });
84            println!("{}", serde_json::to_string_pretty(&output).unwrap());
85            Ok(())
86        }
87        ConfigAction::RemoveKey { fingerprint } => {
88            let mut cfg = config::load_config()?;
89            let before = cfg.keys.len();
90            cfg.keys.retain(|k| k.fingerprint != fingerprint);
91            if cfg.keys.len() == before {
92                return Err(AppError::NotFound(format!(
93                    "no key with fingerprint {fingerprint}"
94                )));
95            }
96            config::save_config(&cfg)?;
97            let output = json!({
98                "action": "key_removed",
99                "fingerprint": fingerprint,
100            });
101            println!("{}", serde_json::to_string(&output).unwrap());
102            Ok(())
103        }
104        ConfigAction::Doctor => {
105            let config_path = config::config_file_path()
106                .map(|p| p.display().to_string())
107                .unwrap_or_else(|_| "unavailable".to_string());
108            let config_exists = std::path::Path::new(&config_path).exists();
109            let providers = ["openrouter"];
110            let mut results = vec![];
111            for provider in &providers {
112                let resolved = config::resolve_api_key(provider, None);
113                results.push(json!({
114                    "provider": provider,
115                    "resolved": resolved.is_some(),
116                    "source": resolved.as_ref().map(|r| r.source),
117                    "masked_value": resolved.as_ref().map(|r| {
118                        use secrecy::ExposeSecret;
119                        mask_key(r.value.expose_secret())
120                    }),
121                }));
122            }
123            let output = json!({
124                "config_path": config_path,
125                "config_exists": config_exists,
126                "providers": results,
127            });
128            println!("{}", serde_json::to_string_pretty(&output).unwrap());
129            Ok(())
130        }
131        ConfigAction::Path => {
132            let path = config::config_file_path()?;
133            let output = json!({
134                "config_path": path.display().to_string(),
135                "exists": path.exists(),
136            });
137            println!("{}", serde_json::to_string(&output).unwrap());
138            Ok(())
139        }
140    }
141}