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