1use crate::cli::{BaseCommand, CliArgs, GraphSubcommand};
7use crate::config::FabrykConfig;
8use crate::{config_handlers, graph_handlers};
9use fabryk_core::Result;
10use fabryk_core::traits::ConfigProvider;
11use std::sync::Arc;
12use tracing_subscriber::EnvFilter;
13
14pub struct FabrykCli<C: ConfigProvider> {
22 name: String,
23 config: Arc<C>,
24 version: String,
25}
26
27impl FabrykCli<FabrykConfig> {
28 pub fn from_args(name: impl Into<String>, args: &CliArgs) -> Result<Self> {
30 let config = FabrykConfig::load(args.config.as_deref())?;
31 Ok(Self::new(name, config))
32 }
33}
34
35impl<C: ConfigProvider> FabrykCli<C> {
36 pub fn new(name: impl Into<String>, config: C) -> Self {
38 Self {
39 name: name.into(),
40 config: Arc::new(config),
41 version: env!("CARGO_PKG_VERSION").to_string(),
42 }
43 }
44
45 pub fn with_version(mut self, version: impl Into<String>) -> Self {
47 self.version = version.into();
48 self
49 }
50
51 pub fn config(&self) -> &C {
53 &self.config
54 }
55
56 pub fn init_logging(&self, verbose: bool, quiet: bool) {
60 let filter = if std::env::var("RUST_LOG").is_ok() {
61 EnvFilter::from_default_env()
62 } else if quiet {
63 EnvFilter::new("warn")
64 } else if verbose {
65 EnvFilter::new("debug")
66 } else {
67 EnvFilter::new("info")
68 };
69
70 let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init();
72 }
73
74 pub async fn run(&self, args: CliArgs) -> Result<()> {
76 self.init_logging(args.verbose, args.quiet);
77
78 match args.command {
79 Some(BaseCommand::Version) => {
80 println!("{} {}", self.name, self.version);
81 Ok(())
82 }
83 Some(BaseCommand::Health) => {
84 println!("{}: healthy", self.name);
85 Ok(())
86 }
87 Some(BaseCommand::Serve { port }) => {
88 println!("Starting {} server on port {}...", self.name, port);
89 Ok(())
91 }
92 Some(BaseCommand::Index { force, check }) => {
93 if check {
94 println!("Checking index freshness...");
95 } else {
96 println!("Building index{}...", if force { " (forced)" } else { "" });
97 }
98 Ok(())
100 }
101 Some(BaseCommand::Graph(graph_cmd)) => self.handle_graph(graph_cmd.command).await,
102 Some(BaseCommand::Config(config_cmd)) => {
103 config_handlers::handle_config_command(args.config.as_deref(), config_cmd.command)
104 }
105 #[cfg(feature = "vector-fastembed")]
106 Some(BaseCommand::Vectordb(cmd)) => {
107 crate::vectordb_handlers::handle_vectordb_command(cmd.command)
108 }
109 None => {
110 println!("{} {} — use --help for usage", self.name, self.version);
111 Ok(())
112 }
113 }
114 }
115
116 async fn handle_graph(&self, command: GraphSubcommand) -> Result<()> {
118 match command {
119 GraphSubcommand::Build {
120 output: _,
121 dry_run: _,
122 } => {
123 println!(
126 "Graph build requires a domain-specific extractor. \
127 Override handle_graph() in your domain CLI."
128 );
129 Ok(())
130 }
131 GraphSubcommand::Validate => graph_handlers::handle_validate(&*self.config).await,
132 GraphSubcommand::Stats => graph_handlers::handle_stats(&*self.config).await,
133 GraphSubcommand::Query { id, query_type, to } => {
134 let options = graph_handlers::QueryOptions { id, query_type, to };
135 graph_handlers::handle_query(&*self.config, options).await
136 }
137 }
138 }
139}
140
141#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::cli::CliArgs;
149 use clap::Parser;
150 use std::path::PathBuf;
151
152 #[derive(Clone)]
153 struct TestConfig {
154 base: PathBuf,
155 }
156
157 impl ConfigProvider for TestConfig {
158 fn project_name(&self) -> &str {
159 "test-app"
160 }
161
162 fn base_path(&self) -> Result<PathBuf> {
163 Ok(self.base.clone())
164 }
165
166 fn content_path(&self, content_type: &str) -> Result<PathBuf> {
167 Ok(self.base.join(content_type))
168 }
169 }
170
171 fn test_config() -> TestConfig {
172 TestConfig {
173 base: PathBuf::from("/tmp/test"),
174 }
175 }
176
177 #[test]
178 fn test_fabryk_cli_new() {
179 let cli = FabrykCli::new("my-app", test_config());
180 assert_eq!(cli.name, "my-app");
181 assert_eq!(cli.config().project_name(), "test-app");
182 }
183
184 #[test]
185 fn test_fabryk_cli_with_version() {
186 let cli = FabrykCli::new("my-app", test_config()).with_version("1.2.3");
187 assert_eq!(cli.version, "1.2.3");
188 }
189
190 #[test]
191 fn test_fabryk_cli_config_access() {
192 let cli = FabrykCli::new("app", test_config());
193 assert_eq!(
194 cli.config().base_path().unwrap(),
195 PathBuf::from("/tmp/test")
196 );
197 }
198
199 #[tokio::test]
200 async fn test_run_version_command() {
201 let cli = FabrykCli::new("test-app", test_config()).with_version("0.1.0");
202 let args = CliArgs::parse_from(["test", "version"]);
203 let result = cli.run(args).await;
204 assert!(result.is_ok());
205 }
206
207 #[tokio::test]
208 async fn test_run_health_command() {
209 let cli = FabrykCli::new("test-app", test_config());
210 let args = CliArgs::parse_from(["test", "health"]);
211 let result = cli.run(args).await;
212 assert!(result.is_ok());
213 }
214
215 #[tokio::test]
216 async fn test_run_no_command() {
217 let cli = FabrykCli::new("test-app", test_config()).with_version("0.1.0");
218 let args = CliArgs::parse_from(["test"]);
219 let result = cli.run(args).await;
220 assert!(result.is_ok());
221 }
222
223 #[tokio::test]
224 async fn test_run_serve_command() {
225 let cli = FabrykCli::new("test-app", test_config());
226 let args = CliArgs::parse_from(["test", "serve", "--port", "9090"]);
227 let result = cli.run(args).await;
228 assert!(result.is_ok());
229 }
230
231 #[tokio::test]
232 async fn test_run_index_command() {
233 let cli = FabrykCli::new("test-app", test_config());
234 let args = CliArgs::parse_from(["test", "index"]);
235 let result = cli.run(args).await;
236 assert!(result.is_ok());
237 }
238
239 #[tokio::test]
240 async fn test_run_index_check() {
241 let cli = FabrykCli::new("test-app", test_config());
242 let args = CliArgs::parse_from(["test", "index", "--check"]);
243 let result = cli.run(args).await;
244 assert!(result.is_ok());
245 }
246
247 #[test]
248 fn test_init_logging_default() {
249 let cli = FabrykCli::new("test", test_config());
250 cli.init_logging(false, false);
252 }
253
254 #[test]
255 fn test_init_logging_verbose() {
256 let cli = FabrykCli::new("test", test_config());
257 cli.init_logging(true, false);
258 }
259
260 #[test]
261 fn test_init_logging_quiet() {
262 let cli = FabrykCli::new("test", test_config());
263 cli.init_logging(false, true);
264 }
265
266 #[test]
271 fn test_fabryk_cli_from_args_default() {
272 let args = CliArgs::parse_from(["test"]);
273 let cli = FabrykCli::from_args("test-app", &args).unwrap();
274 assert_eq!(cli.config().project_name(), "fabryk");
275 }
276
277 #[test]
278 fn test_fabryk_cli_from_args_with_file() {
279 let dir = tempfile::TempDir::new().unwrap();
280 let path = dir.path().join("config.toml");
281 std::fs::write(
282 &path,
283 r#"
284 project_name = "from-file"
285 [server]
286 port = 9090
287 "#,
288 )
289 .unwrap();
290
291 let args = CliArgs::parse_from(["test", "--config", path.to_str().unwrap()]);
292 let cli = FabrykCli::from_args("test-app", &args).unwrap();
293 assert_eq!(cli.config().project_name(), "from-file");
294 }
295
296 #[tokio::test]
297 async fn test_fabryk_cli_config_command_dispatch() {
298 let cli = FabrykCli::new("test-app", test_config());
299 let args = CliArgs::parse_from(["test", "config", "path"]);
300 let result = cli.run(args).await;
301 assert!(result.is_ok());
302 }
303}