Skip to main content

systemprompt_cli/commands/admin/setup/
mod.rs

1//! Interactive and non-interactive setup wizard for a local environment.
2//!
3//! Drives `PostgreSQL` provisioning, secret collection, profile generation, and
4//! optional migrations. [`SetupArgs`] captures the CLI flags and environment
5//! overrides; [`execute`] dispatches to the wizard, which writes a profile and
6//! secrets file under `.systemprompt/`.
7
8pub mod ai_config;
9pub mod catalog;
10pub mod common;
11pub mod ddl;
12mod docker;
13pub mod docker_compose;
14mod docker_database;
15pub mod postgres;
16mod profile;
17mod profile_sections;
18mod secrets;
19mod types;
20mod wizard;
21mod wizard_dry_run;
22mod wizard_prompts;
23
24use crate::shared::CommandOutput;
25use anyhow::Result;
26use clap::Args;
27use systemprompt_models::none_if_blank;
28
29pub use secrets::SecretsData;
30pub use types::*;
31
32#[derive(Debug, Args)]
33#[expect(
34    clippy::struct_excessive_bools,
35    reason = "each bool is an independent clap CLI flag, not a state machine"
36)]
37pub struct SetupArgs {
38    #[arg(
39        short,
40        long,
41        help = "Target environment name (e.g., dev, staging, prod)"
42    )]
43    pub environment: Option<String>,
44
45    #[arg(
46        long,
47        help = "Use Docker for PostgreSQL (default: use existing installation)"
48    )]
49    pub docker: bool,
50
51    #[arg(
52        long,
53        env = "SYSTEMPROMPT_DB_HOST",
54        default_value = "localhost",
55        help = "PostgreSQL host"
56    )]
57    pub db_host: String,
58
59    #[arg(
60        long,
61        env = "SYSTEMPROMPT_DB_PORT",
62        default_value = "5432",
63        help = "PostgreSQL port"
64    )]
65    pub db_port: u16,
66
67    #[arg(
68        long,
69        env = "SYSTEMPROMPT_DB_USER",
70        help = "PostgreSQL user (default: systemprompt_`<env>`)"
71    )]
72    pub db_user: Option<String>,
73
74    #[arg(
75        long,
76        env = "SYSTEMPROMPT_DB_PASSWORD",
77        help = "PostgreSQL password (auto-generated if not provided)"
78    )]
79    pub db_password: Option<String>,
80
81    #[arg(
82        long,
83        env = "SYSTEMPROMPT_DB_NAME",
84        help = "PostgreSQL database name (default: systemprompt_`<env>`)"
85    )]
86    pub db_name: Option<String>,
87
88    #[arg(long, env = "GEMINI_API_KEY", help = "Google AI (Gemini) API key")]
89    pub gemini_key: Option<String>,
90
91    #[arg(long, env = "ANTHROPIC_API_KEY", help = "Anthropic (Claude) API key")]
92    pub anthropic_key: Option<String>,
93
94    #[arg(long, env = "OPENAI_API_KEY", help = "OpenAI (GPT) API key")]
95    pub openai_key: Option<String>,
96
97    #[arg(long, env = "GITHUB_TOKEN", help = "GitHub token (optional)")]
98    pub github_token: Option<String>,
99
100    #[arg(
101        long,
102        env = "SYSTEMPROMPT_DEFAULT_PROVIDER",
103        help = "Provider to make the default (gemini | anthropic | openai); must have a key. \
104                In interactive mode the selected provider is used instead."
105    )]
106    pub default_provider: Option<String>,
107
108    #[arg(long, help = "Run database migrations after setup")]
109    pub migrate: bool,
110
111    #[arg(
112        long,
113        conflicts_with = "migrate",
114        help = "Skip migrations (non-interactive default)"
115    )]
116    pub no_migrate: bool,
117
118    #[arg(long, help = "Preview setup without creating files or making changes")]
119    pub dry_run: bool,
120
121    #[arg(short = 'y', long, help = "Skip confirmation prompts")]
122    pub yes: bool,
123
124    #[arg(
125        long,
126        help = "Overwrite existing profile/secrets files (default: preserve them)"
127    )]
128    pub force: bool,
129}
130
131impl SetupArgs {
132    pub fn effective_db_user(&self, env_name: &str) -> String {
133        self.db_user
134            .clone()
135            .unwrap_or_else(|| format!("systemprompt_{}", env_name))
136    }
137
138    pub fn effective_db_name(&self, env_name: &str) -> String {
139        self.db_name
140            .clone()
141            .unwrap_or_else(|| format!("systemprompt_{}", env_name))
142    }
143
144    pub const fn has_ai_provider(&self) -> bool {
145        self.gemini_key.is_some() || self.anthropic_key.is_some() || self.openai_key.is_some()
146    }
147
148    #[must_use]
149    fn normalized(mut self) -> Self {
150        self.gemini_key = none_if_blank(self.gemini_key);
151        self.anthropic_key = none_if_blank(self.anthropic_key);
152        self.openai_key = none_if_blank(self.openai_key);
153        self.github_token = none_if_blank(self.github_token);
154        self.default_provider = none_if_blank(self.default_provider);
155        self
156    }
157}
158
159pub async fn execute(
160    args: SetupArgs,
161    ctx: &crate::context::CommandContext,
162) -> Result<CommandOutput> {
163    wizard::execute(args.normalized(), ctx.prompter(), &ctx.cli).await
164}