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//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11pub mod ai_config;
12pub mod catalog;
13pub mod common;
14pub mod ddl;
15pub mod docker;
16pub mod docker_compose;
17mod docker_database;
18pub mod postgres;
19mod profile;
20mod profile_sections;
21pub mod secrets;
22pub mod services_files;
23mod types;
24pub mod wizard;
25mod wizard_dry_run;
26#[doc(hidden)]
27pub mod wizard_prompts;
28
29use crate::shared::CommandOutput;
30use anyhow::Result;
31use clap::Args;
32use systemprompt_models::none_if_blank;
33
34pub use secrets::SecretsData;
35pub use types::*;
36
37#[derive(Debug, Args)]
38#[expect(
39    clippy::struct_excessive_bools,
40    reason = "each bool is an independent clap CLI flag, not a state machine"
41)]
42pub struct SetupArgs {
43    #[arg(
44        short,
45        long,
46        help = "Target environment name (e.g., dev, staging, prod)"
47    )]
48    pub environment: Option<String>,
49
50    #[arg(
51        long,
52        help = "Use Docker for PostgreSQL (default: use existing installation)"
53    )]
54    pub docker: bool,
55
56    #[arg(
57        long,
58        env = "SYSTEMPROMPT_DB_HOST",
59        default_value = "localhost",
60        help = "PostgreSQL host"
61    )]
62    pub db_host: String,
63
64    #[arg(
65        long,
66        env = "SYSTEMPROMPT_DB_PORT",
67        default_value = "5432",
68        help = "PostgreSQL port"
69    )]
70    pub db_port: u16,
71
72    #[arg(
73        long,
74        default_value = "0",
75        help = "Shift every locally-bound MCP and agent port by this amount"
76    )]
77    pub port_offset: u16,
78
79    #[arg(
80        long,
81        env = "SYSTEMPROMPT_DB_USER",
82        hide_env_values = true,
83        help = "PostgreSQL user (default: systemprompt_`<env>`)"
84    )]
85    pub db_user: Option<String>,
86
87    #[arg(
88        long,
89        env = "SYSTEMPROMPT_DB_PASSWORD",
90        hide_env_values = true,
91        help = "PostgreSQL password (auto-generated if not provided)"
92    )]
93    pub db_password: Option<String>,
94
95    #[arg(
96        long,
97        env = "SYSTEMPROMPT_DB_NAME",
98        help = "PostgreSQL database name (default: systemprompt_`<env>`)"
99    )]
100    pub db_name: Option<String>,
101
102    #[arg(
103        long,
104        env = "GEMINI_API_KEY",
105        hide_env_values = true,
106        help = "Google AI (Gemini) API key"
107    )]
108    pub gemini_key: Option<String>,
109
110    #[arg(
111        long,
112        env = "ANTHROPIC_API_KEY",
113        hide_env_values = true,
114        help = "Anthropic (Claude) API key"
115    )]
116    pub anthropic_key: Option<String>,
117
118    #[arg(
119        long,
120        env = "OPENAI_API_KEY",
121        hide_env_values = true,
122        help = "OpenAI (GPT) API key"
123    )]
124    pub openai_key: Option<String>,
125
126    #[arg(
127        long,
128        env = "GITHUB_TOKEN",
129        hide_env_values = true,
130        help = "GitHub token (optional)"
131    )]
132    pub github_token: Option<String>,
133
134    #[arg(
135        long,
136        env = "SYSTEMPROMPT_DEFAULT_PROVIDER",
137        help = "Provider to make the default (gemini | anthropic | openai); must have a key. \
138                In interactive mode the selected provider is used instead."
139    )]
140    pub default_provider: Option<String>,
141
142    #[arg(
143        long,
144        env = "SYSTEMPROMPT_ADMIN_EMAIL",
145        hide_env_values = true,
146        help = "Email identifying the platform admin on sign-in and consent screens; required non-interactively"
147    )]
148    pub admin_email: Option<String>,
149
150    #[arg(long, help = "Run database migrations after setup")]
151    pub migrate: bool,
152
153    #[arg(
154        long,
155        conflicts_with = "migrate",
156        help = "Skip migrations (non-interactive default)"
157    )]
158    pub no_migrate: bool,
159
160    #[arg(long, help = "Preview setup without creating files or making changes")]
161    pub dry_run: bool,
162
163    #[arg(short = 'y', long, help = "Skip confirmation prompts")]
164    pub yes: bool,
165
166    #[arg(
167        long,
168        help = "Overwrite existing profile/secrets files (default: preserve them)"
169    )]
170    pub force: bool,
171}
172
173impl SetupArgs {
174    pub fn effective_db_user(&self, env_name: &str) -> String {
175        self.db_user
176            .clone()
177            .unwrap_or_else(|| format!("systemprompt_{}", env_name))
178    }
179
180    pub fn effective_db_name(&self, env_name: &str) -> String {
181        self.db_name
182            .clone()
183            .unwrap_or_else(|| format!("systemprompt_{}", env_name))
184    }
185
186    pub const fn has_ai_provider(&self) -> bool {
187        self.gemini_key.is_some() || self.anthropic_key.is_some() || self.openai_key.is_some()
188    }
189
190    #[must_use]
191    fn normalized(mut self) -> Self {
192        self.gemini_key = none_if_blank(self.gemini_key);
193        self.anthropic_key = none_if_blank(self.anthropic_key);
194        self.openai_key = none_if_blank(self.openai_key);
195        self.github_token = none_if_blank(self.github_token);
196        self.default_provider = none_if_blank(self.default_provider);
197        self.admin_email = none_if_blank(self.admin_email);
198        self
199    }
200}
201
202pub async fn execute(
203    args: SetupArgs,
204    ctx: &crate::context::CommandContext,
205) -> Result<CommandOutput> {
206    wizard::execute(args.normalized(), ctx.prompter(), &ctx.cli).await
207}