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        /// GAP-SG-34: no-op; JSON is always emitted on stdout.
22        #[arg(long, hide = true)]
23        json: bool,
24    },
25    /// List stored API keys (masked) with fingerprints.
26    ListKeys {
27        /// GAP-SG-34: no-op; JSON is always emitted on stdout.
28        #[arg(long, hide = true)]
29        json: bool,
30    },
31    /// Remove an API key by its fingerprint.
32    RemoveKey {
33        fingerprint: String,
34        /// GAP-SG-34: no-op; JSON is always emitted on stdout.
35        #[arg(long, hide = true)]
36        json: bool,
37    },
38    /// Diagnose which layer won for each provider (env/config/cli).
39    Doctor {
40        /// GAP-SG-34: no-op; JSON is always emitted on stdout.
41        #[arg(long, hide = true)]
42        json: bool,
43    },
44    /// Print the resolved XDG config file path.
45    Path {
46        /// GAP-SG-34: no-op; JSON is always emitted on stdout.
47        #[arg(long, hide = true)]
48        json: bool,
49    },
50}
51
52pub fn run(args: ConfigArgs) -> Result<(), AppError> {
53    match args.action {
54        ConfigAction::AddKey {
55            provider,
56            from_stdin,
57            json: _,
58        } => {
59            let key = if from_stdin {
60                let mut buf = String::new();
61                io::stdin().read_to_string(&mut buf).map_err(AppError::Io)?;
62                buf.trim().to_string()
63            } else {
64                return Err(AppError::Validation(
65                    "--from-stdin is required to avoid shell history exposure".into(),
66                ));
67            };
68            if key.is_empty() {
69                return Err(AppError::Validation("API key cannot be empty".into()));
70            }
71            let fingerprint = compute_fingerprint(&key);
72            let entry = ApiKeyEntry {
73                provider: provider.clone(),
74                value: key,
75                added_at: chrono::Utc::now().to_rfc3339(),
76                fingerprint: fingerprint.clone(),
77            };
78            let mut cfg = config::load_config()?;
79            cfg.keys.retain(|k| k.provider != provider);
80            cfg.keys.push(entry);
81            config::save_config(&cfg)?;
82            let output = json!({
83                "action": "key_added",
84                "provider": provider,
85                "fingerprint": fingerprint,
86            });
87            println!("{}", serde_json::to_string(&output)?);
88            Ok(())
89        }
90        ConfigAction::ListKeys { json: _ } => {
91            let cfg = config::load_config()?;
92            let keys: Vec<_> = cfg
93                .keys
94                .iter()
95                .map(|k| {
96                    json!({
97                        "provider": k.provider,
98                        "fingerprint": k.fingerprint,
99                        "masked_value": mask_key(&k.value),
100                        "added_at": k.added_at,
101                    })
102                })
103                .collect();
104            let output = json!({ "keys": keys });
105            println!("{}", serde_json::to_string_pretty(&output)?);
106            Ok(())
107        }
108        ConfigAction::RemoveKey {
109            fingerprint,
110            json: _,
111        } => {
112            let mut cfg = config::load_config()?;
113            let before = cfg.keys.len();
114            cfg.keys.retain(|k| k.fingerprint != fingerprint);
115            if cfg.keys.len() == before {
116                return Err(AppError::NotFound(format!(
117                    "no key with fingerprint {fingerprint}"
118                )));
119            }
120            config::save_config(&cfg)?;
121            let output = json!({
122                "action": "key_removed",
123                "fingerprint": fingerprint,
124            });
125            println!("{}", serde_json::to_string(&output)?);
126            Ok(())
127        }
128        ConfigAction::Doctor { json: _ } => {
129            let config_path = config::config_file_path()
130                .map(|p| p.display().to_string())
131                .unwrap_or_else(|_| "unavailable".to_string());
132            let config_exists = std::path::Path::new(&config_path).exists();
133            let providers = ["openrouter"];
134            let mut results = vec![];
135            for provider in &providers {
136                let resolved = config::resolve_api_key(provider, None);
137                results.push(json!({
138                    "provider": provider,
139                    "resolved": resolved.is_some(),
140                    "source": resolved.as_ref().map(|r| r.source),
141                    "masked_value": resolved.as_ref().map(|r| {
142                        use secrecy::ExposeSecret;
143                        mask_key(r.value.expose_secret())
144                    }),
145                }));
146            }
147            let output = json!({
148                "config_path": config_path,
149                "config_exists": config_exists,
150                "providers": results,
151            });
152            println!("{}", serde_json::to_string_pretty(&output)?);
153            Ok(())
154        }
155        ConfigAction::Path { json: _ } => {
156            let path = config::config_file_path()?;
157            let output = json!({
158                "config_path": path.display().to_string(),
159                "exists": path.exists(),
160            });
161            println!("{}", serde_json::to_string(&output)?);
162            Ok(())
163        }
164    }
165}