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