sqlite_graphrag/commands/
config_cmd.rs1use 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 AddKey {
17 #[arg(long)]
18 provider: String,
19 #[arg(long, default_value_t = true)]
20 from_stdin: bool,
21 },
22 ListKeys,
24 RemoveKey { fingerprint: String },
26 Doctor,
28 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}