Skip to main content

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!(
81                    "repo: {}",
82                    crate::redact::redact_url(&git_config.url)
83                )),
84            });
85        } else {
86            sources.push(SourceStatus {
87                name: format!("git:{}", name),
88                configured: true,
89                healthy: false,
90                notes: Some("git binary not found".to_string()),
91            });
92        }
93    }
94
95    // S3 connectors
96    for (name, s3_config) in &config.connectors.s3 {
97        sources.push(SourceStatus {
98            name: format!("s3:{}", name),
99            configured: true,
100            healthy: true,
101            notes: Some(format!("bucket: {}", s3_config.bucket)),
102        });
103    }
104
105    // Script connectors
106    for (name, script_config) in &config.connectors.script {
107        let path_exists = script_config.path.exists();
108        sources.push(SourceStatus {
109            name: format!("script:{}", name),
110            configured: true,
111            healthy: path_exists,
112            notes: if path_exists {
113                Some(format!("path: {}", script_config.path.display()))
114            } else {
115                Some(format!(
116                    "script not found: {}",
117                    script_config.path.display()
118                ))
119            },
120        });
121    }
122
123    sources
124}
125
126/// CLI entry point for `ctx sources`.
127///
128/// Calls [`get_sources`] and prints a formatted table of connector statuses to stdout.
129pub fn list_sources(config: &Config) -> Result<()> {
130    let sources = get_sources(config);
131
132    println!("{:<16} {:<12} HEALTHY", "CONNECTOR", "STATUS");
133    for s in &sources {
134        let status_str = if s.configured { "OK" } else { "NOT CONFIGURED" };
135        println!("{:<16} {:<12} {}", s.name, status_str, s.healthy);
136    }
137
138    Ok(())
139}