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