Skip to main content

systemprompt_cli/commands/cloud/profile/
templates.rs

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