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