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}