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