systemprompt_cli/commands/cloud/profile/
templates.rs1use 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_jwt_secret};
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 sync_token: Option<&str>,
119 secrets_path: &Path,
120 is_cloud_tenant: bool,
121) -> Result<()> {
122 use serde_json::json;
123 use systemprompt_models::Profile;
124
125 if Profile::is_masked_database_url(db_urls.external) {
126 CliService::warning(
127 "Database URL appears to be masked. Credentials may not work correctly.",
128 );
129 CliService::warning(
130 "Run 'systemprompt cloud tenant refresh-credentials' to fetch real credentials.",
131 );
132 }
133
134 if let Some(internal) = db_urls.internal {
135 if Profile::is_masked_database_url(internal) {
136 CliService::warning(
137 "Internal database URL appears to be masked. Credentials may not work correctly.",
138 );
139 }
140 }
141
142 if sync_token.is_none() && is_cloud_tenant {
143 CliService::warning("Sync token not available. Cloud sync will not work.");
144 CliService::warning("Run 'systemprompt cloud tenant rotate-sync-token' to generate one.");
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 "jwt_secret": generate_jwt_secret(),
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 if let Some(token) = sync_token {
166 secrets["sync_token"] = json!(token);
167 }
168
169 let content = serde_json::to_string_pretty(&secrets).context("Failed to serialize secrets")?;
170
171 std::fs::write(secrets_path, content)
172 .with_context(|| format!("Failed to write {}", secrets_path.display()))?;
173
174 #[cfg(unix)]
175 {
176 use std::os::unix::fs::PermissionsExt;
177 let permissions = std::fs::Permissions::from_mode(0o600);
178 std::fs::set_permissions(secrets_path, permissions)
179 .with_context(|| format!("Failed to set permissions on {}", secrets_path.display()))?;
180 }
181
182 Ok(())
183}
184
185
186pub fn get_services_path() -> Result<String> {
187 if let Ok(path) = std::env::var("SYSTEMPROMPT_SERVICES_PATH") {
188 return Ok(path);
189 }
190
191 let cwd = std::env::current_dir().context("Failed to get current directory")?;
192 let services_path = cwd.join("services");
193
194 Ok(services_path.to_string_lossy().to_string())
195}
196
197pub async fn validate_connection(db_url: &str) -> bool {
198 use tokio::time::{timeout, Duration};
199
200 let result = timeout(Duration::from_secs(5), async {
201 sqlx::postgres::PgPoolOptions::new()
202 .max_connections(1)
203 .connect(db_url)
204 .await
205 })
206 .await;
207
208 matches!(result, Ok(Ok(_)))
209}
210
211pub async fn run_migrations_cmd(profile_path: &Path) -> Result<()> {
212 use std::process::Command;
213
214 CliService::info("Running database migrations...");
215
216 let current_exe = std::env::current_exe().context("Failed to get executable path")?;
217 let profile_path_str = profile_path.to_string_lossy();
218
219 let output = Command::new(¤t_exe)
220 .args(CliPaths::db_migrate_args())
221 .env("SYSTEMPROMPT_PROFILE", profile_path_str.as_ref())
222 .output()
223 .context("Failed to run migrations")?;
224
225 if output.status.success() {
226 CliService::success("Migrations completed");
227 return Ok(());
228 }
229
230 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
231 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
232
233 let error_output = if !stderr.is_empty() {
234 stderr
235 } else if !stdout.is_empty() {
236 stdout
237 } else {
238 "Unknown error (no output)".to_string()
239 };
240
241 anyhow::bail!("Migration failed: {}", error_output)
242}
243
244pub fn update_ai_config_default_provider(provider: &str) -> Result<()> {
245 let services_path = get_services_path()?;
246 let ai_dir = Path::new(&services_path).join("ai");
247 let ai_config_path = ai_dir.join("config.yaml");
248
249 if !ai_config_path.exists() {
250 CliService::warning("AI config not found. Creating services/ai/config.yaml");
251 CliService::info("Run 'systemprompt cloud init' for full project setup");
252
253 std::fs::create_dir_all(&ai_dir)
254 .with_context(|| format!("Failed to create directory {}", ai_dir.display()))?;
255 std::fs::write(&ai_config_path, ai_config(provider))
256 .with_context(|| format!("Failed to write {}", ai_config_path.display()))?;
257 CliService::success(&format!("Created: {}", ai_config_path.display()));
258 return Ok(());
259 }
260
261 let content = std::fs::read_to_string(&ai_config_path)
262 .with_context(|| format!("Failed to read {}", ai_config_path.display()))?;
263 let re = Regex::new(r#"default_provider:\s*"?\w+"?"#).context("Failed to compile regex")?;
264 let updated = re.replace(&content, format!(r#"default_provider: "{}""#, provider));
265
266 std::fs::write(&ai_config_path, updated.as_ref())
267 .with_context(|| format!("Failed to write {}", ai_config_path.display()))?;
268 CliService::success(&format!(
269 "Updated default_provider to '{}' in AI config",
270 provider
271 ));
272 Ok(())
273}