systemprompt_cli/commands/cloud/doctor/
mod.rs1mod checks;
15
16pub(in crate::commands::cloud) use checks::resolve_signing_key_path;
17pub use checks::{
18 check_extension_configs, check_profile_valid, check_provider_secrets, check_proxy_topology,
19 check_required_secrets, check_signing_key,
20};
21
22use std::collections::HashMap;
23use std::path::Path;
24
25use anyhow::{Result, anyhow, bail};
26use systemprompt_cloud::ProfilePath;
27use systemprompt_logging::CliService;
28use systemprompt_models::Profile;
29
30use super::deploy::resolve_profile;
31use crate::cli_settings::CliConfig;
32use crate::interactive::Prompter;
33use systemprompt_cloud::secrets_env::load_secrets_json;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum CheckStatus {
37 Pass,
38 Warn,
39 Fail,
40}
41
42#[derive(Debug)]
43pub struct CheckResult {
44 pub name: &'static str,
45 pub status: CheckStatus,
46 pub detail: String,
47}
48
49pub(in crate::commands::cloud) struct DoctorReport {
50 checks: Vec<CheckResult>,
51}
52
53impl DoctorReport {
54 pub(in crate::commands::cloud) fn has_blocking(&self) -> bool {
55 self.checks.iter().any(|c| c.status == CheckStatus::Fail)
56 }
57
58 pub(in crate::commands::cloud) fn render(&self) {
59 CliService::section("Deploy preflight");
60 for check in &self.checks {
61 let line = format!("{}: {}", check.name, check.detail);
62 match check.status {
63 CheckStatus::Pass => CliService::success(&line),
64 CheckStatus::Warn => CliService::warning(&line),
65 CheckStatus::Fail => CliService::error(&line),
66 }
67 }
68 }
69}
70
71pub(in crate::commands::cloud) async fn run(profile: &Profile, profile_dir: &Path) -> DoctorReport {
72 let mut checks = vec![check_profile_valid(profile)];
73
74 let secrets_path = ProfilePath::Secrets.resolve(profile_dir);
75 let secrets = load_secrets_json(&secrets_path).unwrap_or_else(|_| {
76 checks.push(CheckResult::fail(
77 "secrets-file",
78 format!(
79 "secrets.json not found or unreadable at {}",
80 secrets_path.display()
81 ),
82 ));
83 HashMap::new()
84 });
85
86 checks.push(check_required_secrets(&secrets));
87 checks.push(check_signing_key(profile, profile_dir, &secrets));
88 checks.push(check_provider_secrets(profile, &secrets));
89 checks.push(check_extension_configs(profile));
90 checks.push(check_proxy_topology(profile));
91 checks.push(checks::check_governance_hook_url(profile));
92 checks.push(checks::check_database_reachable(&secrets).await);
93
94 DoctorReport { checks }
95}
96
97pub(in crate::commands::cloud) async fn execute(
98 profile_name: Option<String>,
99 prompter: &dyn Prompter,
100 config: &CliConfig,
101) -> Result<()> {
102 let (profile, profile_path) = resolve_profile(prompter, profile_name.as_deref(), config)?;
103 let profile_dir = profile_path
104 .parent()
105 .ok_or_else(|| anyhow!("Invalid profile path"))?;
106
107 let report = run(&profile, profile_dir).await;
108 report.render();
109
110 if report.has_blocking() {
111 bail!("Deploy preflight failed — fix the items above before deploying.");
112 }
113 CliService::success("Deploy preflight passed");
114 Ok(())
115}