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