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 #[arg(long, hide = true)]
23 json: bool,
24 },
25 ListKeys {
27 #[arg(long, hide = true)]
29 json: bool,
30 },
31 RemoveKey {
33 fingerprint: String,
34 #[arg(long, hide = true)]
36 json: bool,
37 },
38 Doctor {
40 #[arg(long, hide = true)]
42 json: bool,
43 },
44 Path {
46 #[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}