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 None => {
106 println!("{} {} — use --help for usage", self.name, self.version);
107 Ok(())
108 }
109 }
110 }
111
112 async fn handle_graph(&self, command: GraphSubcommand) -> Result<()> {
114 match command {
115 GraphSubcommand::Build {
116 output: _,
117 dry_run: _,
118 } => {
119 println!(
122 "Graph build requires a domain-specific extractor. \
123 Override handle_graph() in your domain CLI."
124 );
125 Ok(())
126 }
127 GraphSubcommand::Validate => graph_handlers::handle_validate(&*self.config).await,
128 GraphSubcommand::Stats => graph_handlers::handle_stats(&*self.config).await,
129 GraphSubcommand::Query { id, query_type, to } => {
130 let options = graph_handlers::QueryOptions { id, query_type, to };
131 graph_handlers::handle_query(&*self.config, options).await
132 }
133 }
134 }
135}
136
137#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::cli::CliArgs;
145 use clap::Parser;
146 use std::path::PathBuf;
147
148 #[derive(Clone)]
149 struct TestConfig {
150 base: PathBuf,
151 }
152
153 impl ConfigProvider for TestConfig {
154 fn project_name(&self) -> &str {
155 "test-app"
156 }
157
158 fn base_path(&self) -> Result<PathBuf> {
159 Ok(self.base.clone())
160 }
161
162 fn content_path(&self, content_type: &str) -> Result<PathBuf> {
163 Ok(self.base.join(content_type))
164 }
165 }
166
167 fn test_config() -> TestConfig {
168 TestConfig {
169 base: PathBuf::from("/tmp/test"),
170 }
171 }
172
173 #[test]
174 fn test_fabryk_cli_new() {
175 let cli = FabrykCli::new("my-app", test_config());
176 assert_eq!(cli.name, "my-app");
177 assert_eq!(cli.config().project_name(), "test-app");
178 }
179
180 #[test]
181 fn test_fabryk_cli_with_version() {
182 let cli = FabrykCli::new("my-app", test_config()).with_version("1.2.3");
183 assert_eq!(cli.version, "1.2.3");
184 }
185
186 #[test]
187 fn test_fabryk_cli_config_access() {
188 let cli = FabrykCli::new("app", test_config());
189 assert_eq!(
190 cli.config().base_path().unwrap(),
191 PathBuf::from("/tmp/test")
192 );
193 }
194
195 #[tokio::test]
196 async fn test_run_version_command() {
197 let cli = FabrykCli::new("test-app", test_config()).with_version("0.1.0");
198 let args = CliArgs::parse_from(["test", "version"]);
199 let result = cli.run(args).await;
200 assert!(result.is_ok());
201 }
202
203 #[tokio::test]
204 async fn test_run_health_command() {
205 let cli = FabrykCli::new("test-app", test_config());
206 let args = CliArgs::parse_from(["test", "health"]);
207 let result = cli.run(args).await;
208 assert!(result.is_ok());
209 }
210
211 #[tokio::test]
212 async fn test_run_no_command() {
213 let cli = FabrykCli::new("test-app", test_config()).with_version("0.1.0");
214 let args = CliArgs::parse_from(["test"]);
215 let result = cli.run(args).await;
216 assert!(result.is_ok());
217 }
218
219 #[tokio::test]
220 async fn test_run_serve_command() {
221 let cli = FabrykCli::new("test-app", test_config());
222 let args = CliArgs::parse_from(["test", "serve", "--port", "9090"]);
223 let result = cli.run(args).await;
224 assert!(result.is_ok());
225 }
226
227 #[tokio::test]
228 async fn test_run_index_command() {
229 let cli = FabrykCli::new("test-app", test_config());
230 let args = CliArgs::parse_from(["test", "index"]);
231 let result = cli.run(args).await;
232 assert!(result.is_ok());
233 }
234
235 #[tokio::test]
236 async fn test_run_index_check() {
237 let cli = FabrykCli::new("test-app", test_config());
238 let args = CliArgs::parse_from(["test", "index", "--check"]);
239 let result = cli.run(args).await;
240 assert!(result.is_ok());
241 }
242
243 #[test]
244 fn test_init_logging_default() {
245 let cli = FabrykCli::new("test", test_config());
246 cli.init_logging(false, false);
248 }
249
250 #[test]
251 fn test_init_logging_verbose() {
252 let cli = FabrykCli::new("test", test_config());
253 cli.init_logging(true, false);
254 }
255
256 #[test]
257 fn test_init_logging_quiet() {
258 let cli = FabrykCli::new("test", test_config());
259 cli.init_logging(false, true);
260 }
261
262 #[test]
267 fn test_fabryk_cli_from_args_default() {
268 let args = CliArgs::parse_from(["test"]);
269 let cli = FabrykCli::from_args("test-app", &args).unwrap();
270 assert_eq!(cli.config().project_name(), "fabryk");
271 }
272
273 #[test]
274 fn test_fabryk_cli_from_args_with_file() {
275 let dir = tempfile::TempDir::new().unwrap();
276 let path = dir.path().join("config.toml");
277 std::fs::write(
278 &path,
279 r#"
280 project_name = "from-file"
281 [server]
282 port = 9090
283 "#,
284 )
285 .unwrap();
286
287 let args = CliArgs::parse_from(["test", "--config", path.to_str().unwrap()]);
288 let cli = FabrykCli::from_args("test-app", &args).unwrap();
289 assert_eq!(cli.config().project_name(), "from-file");
290 }
291
292 #[tokio::test]
293 async fn test_fabryk_cli_config_command_dispatch() {
294 let cli = FabrykCli::new("test-app", test_config());
295 let args = CliArgs::parse_from(["test", "config", "path"]);
296 let result = cli.run(args).await;
297 assert!(result.is_ok());
298 }
299}