Skip to main content

systemprompt_cli/commands/cloud/profile/
templates.rs

1use anyhow::{Context, Result};
2use regex::Regex;
3use std::path::Path;
4use systemprompt_cloud::constants::container;
5use systemprompt_logging::CliService;
6use systemprompt_models::{CliPaths, Profile};
7
8use crate::commands::cloud::init::templates::ai_config;
9
10use crate::cloud::dockerfile::DockerfileBuilder;
11
12pub use crate::shared::profile::{generate_display_name, generate_oauth_at_rest_pepper};
13
14pub fn save_profile(profile: &Profile, profile_path: &Path) -> Result<()> {
15    let header = format!(
16        "# systemprompt.io Profile: {}\n# Generated by 'systemprompt cloud profile create'",
17        profile.display_name
18    );
19
20    crate::shared::profile::save_profile_yaml(profile, profile_path, Some(&header))
21}
22
23pub fn save_dockerfile(path: &Path, profile_name: &str, project_root: &Path) -> Result<()> {
24    let content = DockerfileBuilder::new(project_root)
25        .with_profile(profile_name)
26        .build();
27
28    std::fs::write(path, &content)
29        .with_context(|| format!("Failed to write {}", path.display()))?;
30
31    Ok(())
32}
33
34pub fn save_entrypoint(path: &Path) -> Result<()> {
35    let content = format!(
36        r#"#!/bin/sh
37set -e
38
39echo "Running database migrations..."
40{bin}/systemprompt {db_migrate_cmd}
41
42echo "Starting services..."
43exec {bin}/systemprompt {services_serve_cmd} --foreground
44"#,
45        bin = container::BIN,
46        db_migrate_cmd = CliPaths::db_migrate_cmd(),
47        services_serve_cmd = CliPaths::services_serve_cmd(),
48    );
49
50    if let Some(parent) = path.parent() {
51        std::fs::create_dir_all(parent)
52            .with_context(|| format!("Failed to create directory {}", parent.display()))?;
53    }
54
55    std::fs::write(path, &content)
56        .with_context(|| format!("Failed to write {}", path.display()))?;
57
58    #[cfg(unix)]
59    {
60        use std::os::unix::fs::PermissionsExt;
61        let permissions = std::fs::Permissions::from_mode(0o755);
62        std::fs::set_permissions(path, permissions)
63            .with_context(|| format!("Failed to set permissions on {}", path.display()))?;
64    }
65
66    Ok(())
67}
68
69pub fn save_dockerignore(path: &Path) -> Result<()> {
70    let content = r".git
71.gitignore
72.gitmodules
73target/debug
74target/release/.fingerprint
75target/release/build
76target/release/deps
77target/release/examples
78target/release/incremental
79target/release/.cargo-lock
80.cargo
81.systemprompt/credentials.json
82.systemprompt/tenants.json
83.systemprompt/**/secrets.json
84.systemprompt/docker
85.systemprompt/storage
86.env*
87backup
88docs
89instructions
90*.md
91web/node_modules
92.vscode
93.idea
94logs
95*.log
96plan
97";
98
99    if let Some(parent) = path.parent() {
100        std::fs::create_dir_all(parent)
101            .with_context(|| format!("Failed to create directory {}", parent.display()))?;
102    }
103
104    std::fs::write(path, content).with_context(|| format!("Failed to write {}", path.display()))?;
105
106    Ok(())
107}
108
109#[derive(Debug)]
110pub struct DatabaseUrls<'a> {
111    pub external: &'a str,
112    pub internal: Option<&'a str>,
113}
114
115pub fn save_secrets(
116    db_urls: &DatabaseUrls<'_>,
117    api_keys: &super::api_keys::ApiKeys,
118    secrets_path: &Path,
119    _is_cloud_tenant: bool,
120) -> Result<()> {
121    use serde_json::json;
122    use systemprompt_models::Profile;
123
124    if Profile::is_masked_database_url(db_urls.external) {
125        CliService::warning(
126            "Database URL appears to be masked. Credentials may not work correctly.",
127        );
128        CliService::warning(
129            "Run 'systemprompt cloud tenant refresh-credentials' to fetch real credentials.",
130        );
131    }
132
133    if let Some(internal) = db_urls.internal {
134        if Profile::is_masked_database_url(internal) {
135            CliService::warning(
136                "Internal database URL appears to be masked. Credentials may not work correctly.",
137            );
138        }
139    }
140
141    if let Some(parent) = secrets_path.parent() {
142        std::fs::create_dir_all(parent)
143            .with_context(|| format!("Failed to create directory {}", parent.display()))?;
144    }
145
146    let mut secrets = json!({
147        "oauth_at_rest_pepper": generate_oauth_at_rest_pepper(),
148        "database_url": db_urls.external,
149        "external_database_url": db_urls.external,
150        "gemini": api_keys.gemini,
151        "anthropic": api_keys.anthropic,
152        "openai": api_keys.openai
153    });
154
155    if let Some(internal) = db_urls.internal {
156        secrets["internal_database_url"] = json!(internal);
157    }
158
159    let content = serde_json::to_string_pretty(&secrets).context("Failed to serialize secrets")?;
160
161    std::fs::write(secrets_path, content)
162        .with_context(|| format!("Failed to write {}", secrets_path.display()))?;
163
164    #[cfg(unix)]
165    {
166        use std::os::unix::fs::PermissionsExt;
167        let permissions = std::fs::Permissions::from_mode(0o600);
168        std::fs::set_permissions(secrets_path, permissions)
169            .with_context(|| format!("Failed to set permissions on {}", secrets_path.display()))?;
170    }
171
172    Ok(())
173}
174
175pub fn get_services_path() -> Result<String> {
176    if let Ok(path) = std::env::var("SYSTEMPROMPT_SERVICES_PATH") {
177        return Ok(path);
178    }
179
180    let cwd = std::env::current_dir().context("Failed to get current directory")?;
181    let services_path = cwd.join("services");
182
183    Ok(services_path.to_string_lossy().to_string())
184}
185
186pub async fn validate_connection(db_url: &str) -> bool {
187    use tokio::time::{Duration, timeout};
188
189    let result = timeout(Duration::from_secs(5), async {
190        sqlx::postgres::PgPoolOptions::new()
191            .max_connections(1)
192            .connect(db_url)
193            .await
194    })
195    .await;
196
197    matches!(result, Ok(Ok(_)))
198}
199
200pub fn run_migrations_cmd(profile_path: &Path) -> Result<()> {
201    use std::process::Command;
202
203    CliService::info("Running database migrations...");
204
205    let current_exe = std::env::current_exe().context("Failed to get executable path")?;
206    let profile_path_str = profile_path.to_string_lossy();
207
208    let output = Command::new(&current_exe)
209        .args(CliPaths::db_migrate_args())
210        .env("SYSTEMPROMPT_PROFILE", profile_path_str.as_ref())
211        .output()
212        .context("Failed to run migrations")?;
213
214    if output.status.success() {
215        CliService::success("Migrations completed");
216        return Ok(());
217    }
218
219    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
220    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
221
222    let error_output = if !stderr.is_empty() {
223        stderr
224    } else if !stdout.is_empty() {
225        stdout
226    } else {
227        "Unknown error (no output)".to_string()
228    };
229
230    anyhow::bail!("Migration failed: {}", error_output)
231}
232
233pub fn update_ai_config_default_provider(provider: &str) -> Result<()> {
234    let services_path = get_services_path()?;
235    let ai_dir = Path::new(&services_path).join("ai");
236    let ai_config_path = ai_dir.join("config.yaml");
237
238    if !ai_config_path.exists() {
239        CliService::warning("AI config not found. Creating services/ai/config.yaml");
240        CliService::info("Run 'systemprompt cloud init' for full project setup");
241
242        std::fs::create_dir_all(&ai_dir)
243            .with_context(|| format!("Failed to create directory {}", ai_dir.display()))?;
244        std::fs::write(&ai_config_path, ai_config(provider))
245            .with_context(|| format!("Failed to write {}", ai_config_path.display()))?;
246        CliService::success(&format!("Created: {}", ai_config_path.display()));
247        return Ok(());
248    }
249
250    let content = std::fs::read_to_string(&ai_config_path)
251        .with_context(|| format!("Failed to read {}", ai_config_path.display()))?;
252    let re = Regex::new(r#"default_provider:\s*"?\w+"?"#).context("Failed to compile regex")?;
253    let updated = re.replace(&content, format!(r#"default_provider: "{}""#, provider));
254
255    std::fs::write(&ai_config_path, updated.as_ref())
256        .with_context(|| format!("Failed to write {}", ai_config_path.display()))?;
257    CliService::success(&format!(
258        "Updated default_provider to '{}' in AI config",
259        provider
260    ));
261    Ok(())
262}