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