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