use anyhow::{Context, Result, bail};
use systemprompt_cloud::{
CloudPath, ProfilePath, ProjectContext, TenantStore, TenantType, get_cloud_paths,
};
use systemprompt_logging::CliService;
use systemprompt_identifiers::TenantId;
use super::CreateArgs;
use super::api_keys::{ApiKeys, collect_api_keys};
use super::builders::{CloudProfileBuilder, LocalProfileBuilder};
use super::create_setup::{get_cloud_user, handle_local_tenant_setup};
use super::create_tenant::{get_tenants_by_type, select_tenant, select_tenant_type};
use super::profile_steps::{ensure_unmasked_credentials, resolve_tenant_from_args};
use super::templates::{
DatabaseUrls, get_services_path, save_dockerfile, save_dockerignore, save_entrypoint,
save_profile, save_secrets, update_ai_config_default_provider,
};
use crate::cli_settings::CliConfig;
pub use super::profile_steps::{CreatedProfile, create_profile_for_tenant};
pub async fn execute(args: &CreateArgs, config: &CliConfig) -> Result<()> {
let name = &args.name;
CliService::section(&format!("Create Profile: {}", name));
let cloud_user = get_cloud_user()?;
let ctx = ProjectContext::discover();
let profile_dir = ctx.profile_dir(name);
if profile_dir.exists() {
bail!(
"Profile '{}' already exists at {}\nUse 'systemprompt cloud profile delete {}' first.",
name,
profile_dir.display(),
name
);
}
std::fs::create_dir_all(ctx.profiles_dir())
.with_context(|| format!("Failed to create {}", ctx.profiles_dir().display()))?;
let cloud_paths = get_cloud_paths();
let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
let store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
CliService::warning(&format!("Failed to load tenant store: {}", e));
TenantStore::default()
});
let (tenant, api_keys) = if config.is_interactive() && args.tenant.is_none() {
let tenant_type = select_tenant_type(&store)?;
let eligible_tenants = get_tenants_by_type(&store, tenant_type);
let tenant = select_tenant(&eligible_tenants)?;
if !tenant.has_database_url() {
bail!(
"Tenant '{}' does not have a database URL configured.\nFor local tenants, \
recreate with 'systemprompt cloud tenant create'.",
tenant.name
);
}
CliService::section("API Keys");
let api_keys = collect_api_keys()?;
(tenant, api_keys)
} else {
let tenant = resolve_tenant_from_args(args, &store)?;
if !tenant.has_database_url() {
bail!(
"Tenant '{}' does not have a database URL configured.\nFor local tenants, \
recreate with 'systemprompt cloud tenant create'.",
tenant.name
);
}
let api_keys = ApiKeys::from_options(
args.gemini_key.clone(),
args.anthropic_key.clone(),
args.openai_key.clone(),
)?;
(tenant, api_keys)
};
let tenant = ensure_unmasked_credentials(tenant, &tenants_path).await?;
std::fs::create_dir_all(&profile_dir)
.with_context(|| format!("Failed to create directory {}", profile_dir.display()))?;
std::fs::create_dir_all(ctx.storage_dir()).with_context(|| {
format!(
"Failed to create storage directory {}",
ctx.storage_dir().display()
)
})?;
let secrets_path = ProfilePath::Secrets.resolve(&profile_dir);
let external_url = tenant
.get_local_database_url()
.ok_or_else(|| anyhow::anyhow!("Tenant database URL is required"))?;
let db_urls = DatabaseUrls {
external: external_url,
internal: tenant.internal_database_url.as_deref(),
};
save_secrets(
&db_urls,
&api_keys,
tenant.sync_token.as_deref(),
&secrets_path,
tenant.tenant_type == TenantType::Cloud,
)?;
CliService::success(&format!("Created: {}", secrets_path.display()));
update_ai_config_default_provider(api_keys.selected_provider())?;
let services_path = get_services_path()?;
let profile_path = ProfilePath::Config.resolve(&profile_dir);
let relative_secrets_path = "./secrets.json";
let built_profile = match tenant.tenant_type {
TenantType::Local => LocalProfileBuilder::new(name, relative_secrets_path, &services_path)
.with_tenant_id(TenantId::new(&tenant.id))
.build(),
TenantType::Cloud => {
let mut builder = CloudProfileBuilder::new(name)
.with_tenant_id(TenantId::new(&tenant.id))
.with_external_db_access(tenant.external_db_access)
.with_secrets_path(relative_secrets_path);
if let Some(hostname) = &tenant.hostname {
builder = builder.with_external_url(format!("https://{}", hostname));
}
builder.build()
},
};
save_profile(&built_profile, &profile_path)?;
CliService::success(&format!("Created: {}", profile_path.display()));
let docker_dir = ctx.profile_docker_dir(name);
std::fs::create_dir_all(&docker_dir)
.with_context(|| format!("Failed to create docker directory {}", docker_dir.display()))?;
let dockerfile_path = ctx.profile_dockerfile(name);
save_dockerfile(&dockerfile_path, name, ctx.root())?;
CliService::success(&format!("Created: {}", dockerfile_path.display()));
let entrypoint_path = ctx.profile_entrypoint(name);
save_entrypoint(&entrypoint_path)?;
CliService::success(&format!("Created: {}", entrypoint_path.display()));
let dockerignore_path = ctx.profile_dockerignore(name);
save_dockerignore(&dockerignore_path)?;
CliService::success(&format!("Created: {}", dockerignore_path.display()));
match built_profile.validate() {
Ok(()) => CliService::success("Profile validated"),
Err(e) => CliService::warning(&format!("Validation warning: {}", e)),
}
if tenant.tenant_type == TenantType::Local {
let db_url = tenant
.get_local_database_url()
.ok_or_else(|| anyhow::anyhow!("Tenant database URL is required"))?;
handle_local_tenant_setup(&cloud_user, db_url, &tenant.name, &profile_path).await?;
}
CliService::section("Next Steps");
CliService::info(&format!(
" export SYSTEMPROMPT_PROFILE={}",
profile_path.display()
));
match tenant.tenant_type {
TenantType::Local => CliService::info(" just start"),
TenantType::Cloud => CliService::info(" just deploy"),
}
Ok(())
}