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}