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_manifest::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(long, default_value = "localhost", help = "PostgreSQL host")]
57    pub db_host: String,
58
59    #[arg(long, default_value = "5432", help = "PostgreSQL port")]
60    pub db_port: u16,
61
62    #[arg(
63        long,
64        default_value = "0",
65        help = "Shift every locally-bound MCP and agent port by this amount"
66    )]
67    pub port_offset: u16,
68
69    #[arg(long, help = "PostgreSQL user (default: systemprompt_`<env>`)")]
70    pub db_user: Option<String>,
71
72    #[arg(long, help = "PostgreSQL password (auto-generated if not provided)")]
73    pub db_password: Option<String>,
74
75    #[arg(
76        long,
77        help = "PostgreSQL database name (default: systemprompt_`<env>`)"
78    )]
79    pub db_name: Option<String>,
80
81    #[arg(
82        long,
83        env = "GEMINI_API_KEY",
84        hide_env_values = true,
85        help = "Google AI (Gemini) API key"
86    )]
87    pub gemini_key: Option<String>,
88
89    #[arg(
90        long,
91        env = "ANTHROPIC_API_KEY",
92        hide_env_values = true,
93        help = "Anthropic (Claude) API key"
94    )]
95    pub anthropic_key: Option<String>,
96
97    #[arg(
98        long,
99        env = "OPENAI_API_KEY",
100        hide_env_values = true,
101        help = "OpenAI (GPT) API key"
102    )]
103    pub openai_key: Option<String>,
104
105    #[arg(
106        long,
107        env = "GITHUB_TOKEN",
108        hide_env_values = true,
109        help = "GitHub token (optional)"
110    )]
111    pub github_token: Option<String>,
112
113    #[arg(
114        long,
115        help = "Provider to make the default (gemini | anthropic | openai); must have a key. \
116                In interactive mode the selected provider is used instead."
117    )]
118    pub default_provider: Option<String>,
119
120    #[arg(
121        long,
122        env = "SYSTEMPROMPT_ADMIN_EMAIL",
123        hide_env_values = true,
124        help = "Email identifying the platform admin on sign-in and consent screens; required non-interactively"
125    )]
126    pub admin_email: Option<String>,
127
128    #[arg(long, help = "Run database migrations after setup")]
129    pub migrate: bool,
130
131    #[arg(
132        long,
133        conflicts_with = "migrate",
134        help = "Skip migrations (non-interactive default)"
135    )]
136    pub no_migrate: bool,
137
138    #[arg(long, help = "Preview setup without creating files or making changes")]
139    pub dry_run: bool,
140
141    #[arg(short = 'y', long, help = "Skip confirmation prompts")]
142    pub yes: bool,
143
144    #[arg(
145        long,
146        help = "Overwrite existing profile/secrets files (default: preserve them)"
147    )]
148    pub force: bool,
149}
150
151impl SetupArgs {
152    pub fn effective_db_user(&self, env_name: &str) -> String {
153        self.db_user
154            .clone()
155            .unwrap_or_else(|| format!("systemprompt_{}", env_name))
156    }
157
158    pub fn effective_db_name(&self, env_name: &str) -> String {
159        self.db_name
160            .clone()
161            .unwrap_or_else(|| format!("systemprompt_{}", env_name))
162    }
163
164    pub const fn has_ai_provider(&self) -> bool {
165        self.gemini_key.is_some() || self.anthropic_key.is_some() || self.openai_key.is_some()
166    }
167
168    #[must_use]
169    fn normalized(mut self) -> Self {
170        self.gemini_key = none_if_blank(self.gemini_key);
171        self.anthropic_key = none_if_blank(self.anthropic_key);
172        self.openai_key = none_if_blank(self.openai_key);
173        self.github_token = none_if_blank(self.github_token);
174        self.default_provider = none_if_blank(self.default_provider);
175        self.admin_email = none_if_blank(self.admin_email);
176        self
177    }
178}
179
180pub async fn execute(
181    args: SetupArgs,
182    ctx: &crate::context::CommandContext,
183) -> Result<CommandOutput> {
184    wizard::execute(args.normalized(), ctx.prompter(), &ctx.cli).await
185}