Skip to main content

systemprompt_cli/commands/admin/setup/secrets/
prompts.rs

1//! Interactive provider-key collection for the setup wizard.
2//!
3//! These run only on the interactive path; the non-interactive collector reads
4//! keys straight from flags. [`select_provider_keys`] returns the explicit
5//! default when the user picks a single provider, and `None` when they enter
6//! several keys (the default is then resolved by
7//! [`resolve_interactive_primary`]).
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use anyhow::{Result, anyhow};
13use systemprompt_identifiers::ProviderId;
14use systemprompt_logging::CliService;
15
16use super::SecretsData;
17use crate::interactive::Prompter;
18
19/// Returns `None` for the "enter multiple keys" path; that default is resolved
20/// later from the keys actually present, not at selection time.
21pub fn select_provider_keys(
22    prompter: &dyn Prompter,
23    secrets: &mut SecretsData,
24) -> Result<Option<ProviderId>> {
25    let providers = vec![
26        "Google AI (Gemini) - https://aistudio.google.com/app/apikey".to_owned(),
27        "Anthropic (Claude) - https://console.anthropic.com/api-keys".to_owned(),
28        "OpenAI (GPT) - https://platform.openai.com/api-keys".to_owned(),
29        "Enter multiple keys".to_owned(),
30    ];
31
32    let selection = prompter.select("Select your AI provider", &providers)?;
33
34    match selection {
35        0 => {
36            secrets.gemini = Some(prompt_api_key(prompter, "Gemini API Key")?);
37            Ok(Some(ProviderId::new("gemini")))
38        },
39        1 => {
40            secrets.anthropic = Some(prompt_api_key(prompter, "Anthropic API Key")?);
41            Ok(Some(ProviderId::new("anthropic")))
42        },
43        2 => {
44            secrets.openai = Some(prompt_api_key(prompter, "OpenAI API Key")?);
45            Ok(Some(ProviderId::new("openai")))
46        },
47        3 => {
48            CliService::info("Enter API keys (press Enter to skip any):");
49            if let Some(key) = prompt_optional_api_key(prompter, "Gemini API Key")? {
50                secrets.gemini = Some(key);
51            }
52            if let Some(key) = prompt_optional_api_key(prompter, "Anthropic API Key")? {
53                secrets.anthropic = Some(key);
54            }
55            if let Some(key) = prompt_optional_api_key(prompter, "OpenAI API Key")? {
56                secrets.openai = Some(key);
57            }
58            if let Some(key) = prompt_optional_api_key(prompter, "GitHub Token (optional)")? {
59                secrets.github = Some(key);
60            }
61            Ok(None)
62        },
63        _ => Err(anyhow!("Invalid AI provider option selected")),
64    }
65}
66
67pub fn resolve_interactive_primary(
68    prompter: &dyn Prompter,
69    explicit: Option<ProviderId>,
70    secrets: &SecretsData,
71) -> Result<Option<ProviderId>> {
72    if explicit.is_some() {
73        return Ok(explicit);
74    }
75    match secrets.present_providers().as_slice() {
76        [] => Ok(None),
77        [only] => Ok(Some(ProviderId::new(*only))),
78        present => {
79            let items: Vec<String> = present.iter().map(|p| (*p).to_owned()).collect();
80            let idx = prompter.select("Which provider should be the default?", &items)?;
81            Ok(Some(ProviderId::new(present[idx])))
82        },
83    }
84}
85
86fn prompt_api_key(prompter: &dyn Prompter, prompt: &str) -> Result<String> {
87    let key = prompter.password(prompt)?;
88
89    if key.is_empty() {
90        anyhow::bail!("API key is required");
91    }
92
93    Ok(key)
94}
95
96fn prompt_optional_api_key(prompter: &dyn Prompter, prompt: &str) -> Result<Option<String>> {
97    let key = prompter.password(prompt)?;
98
99    if key.is_empty() {
100        Ok(None)
101    } else {
102        Ok(Some(key))
103    }
104}