context_harness/
sources.rs

1//! Connector health and status listing.
2//!
3//! Reports which connectors are configured and healthy. Used by both the
4//! `ctx sources` CLI command and the `GET /tools/sources` HTTP endpoint.
5//!
6//! # Health Checks
7//!
8//! Each connector performs a lightweight health check:
9//!
10//! | Connector | Healthy When |
11//! |-----------|-------------|
12//! | `filesystem` | Configured root directory exists |
13//! | `git` | `git --version` succeeds (binary is on PATH) |
14//! | `s3` | Always `true` if configured (credentials checked at sync time) |
15//! | `slack`, `jira` | Placeholder — always `NOT CONFIGURED` |
16
17use anyhow::Result;
18use serde::Serialize;
19
20use crate::config::Config;
21
22/// Health and configuration status of a single connector.
23///
24/// This struct matches the `context.sources` response shape defined in
25/// `docs/SCHEMAS.md`. It is serialized as JSON by the HTTP server.
26#[derive(Debug, Clone, Serialize)]
27pub struct SourceStatus {
28    /// The connector name (e.g., `"filesystem"`, `"git"`, `"s3"`).
29    pub name: String,
30    /// Whether the connector has a `[connectors.<name>]` section in the config.
31    pub configured: bool,
32    /// Whether the connector passes its health check.
33    pub healthy: bool,
34    /// Optional diagnostic notes (e.g., `"root directory does not exist"`, `"repo: https://…"`).
35    pub notes: Option<String>,
36}
37
38/// Returns the configuration and health status of all known connectors.
39///
40/// This is the core function used by both the CLI (`ctx sources`) and the
41/// HTTP server (`GET /tools/sources`). It checks each connector's config
42/// and performs a lightweight health probe.
43///
44/// All connector types use named instances (e.g. `filesystem:docs`, `git:platform`).
45pub fn get_sources(config: &Config) -> Vec<SourceStatus> {
46    let mut sources = Vec::new();
47
48    // Filesystem connectors
49    for (name, fs_config) in &config.connectors.filesystem {
50        if fs_config.root.exists() {
51            sources.push(SourceStatus {
52                name: format!("filesystem:{}", name),
53                configured: true,
54                healthy: true,
55                notes: Some(format!("root: {}", fs_config.root.display())),
56            });
57        } else {
58            sources.push(SourceStatus {
59                name: format!("filesystem:{}", name),
60                configured: true,
61                healthy: false,
62                notes: Some("root directory does not exist".to_string()),
63            });
64        }
65    }
66
67    // Git connectors
68    let git_available = std::process::Command::new("git")
69        .arg("--version")
70        .output()
71        .map(|o| o.status.success())
72        .unwrap_or(false);
73
74    for (name, git_config) in &config.connectors.git {
75        if git_available {
76            sources.push(SourceStatus {
77                name: format!("git:{}", name),
78                configured: true,
79                healthy: true,
80                notes: Some(format!("repo: {}", git_config.url)),
81            });
82        } else {
83            sources.push(SourceStatus {
84                name: format!("git:{}", name),
85                configured: true,
86                healthy: false,
87                notes: Some("git binary not found".to_string()),
88            });
89        }
90    }
91
92    // S3 connectors
93    for (name, s3_config) in &config.connectors.s3 {
94        sources.push(SourceStatus {
95            name: format!("s3:{}", name),
96            configured: true,
97            healthy: true,
98            notes: Some(format!("bucket: {}", s3_config.bucket)),
99        });
100    }
101
102    // Script connectors
103    for (name, script_config) in &config.connectors.script {
104        let path_exists = script_config.path.exists();
105        sources.push(SourceStatus {
106            name: format!("script:{}", name),
107            configured: true,
108            healthy: path_exists,
109            notes: if path_exists {
110                Some(format!("path: {}", script_config.path.display()))
111            } else {
112                Some(format!(
113                    "script not found: {}",
114                    script_config.path.display()
115                ))
116            },
117        });
118    }
119
120    sources
121}
122
123/// CLI entry point for `ctx sources`.
124///
125/// Calls [`get_sources`] and prints a formatted table of connector statuses to stdout.
126pub fn list_sources(config: &Config) -> Result<()> {
127    let sources = get_sources(config);
128
129    println!("{:<16} {:<12} HEALTHY", "CONNECTOR", "STATUS");
130    for s in &sources {
131        let status_str = if s.configured { "OK" } else { "NOT CONFIGURED" };
132        println!("{:<16} {:<12} {}", s.name, status_str, s.healthy);
133    }
134
135    Ok(())
136}