1use clap::{Parser, Subcommand};
9
10#[derive(Parser, Debug)]
16#[command(author, about, long_about = None)]
17pub struct CliArgs {
18 #[arg(short, long, env = "FABRYK_CONFIG")]
20 pub config: Option<String>,
21
22 #[arg(short, long)]
24 pub verbose: bool,
25
26 #[arg(short, long)]
28 pub quiet: bool,
29
30 #[command(subcommand)]
32 pub command: Option<BaseCommand>,
33}
34
35#[derive(Subcommand, Debug)]
37pub enum BaseCommand {
38 Serve {
40 #[arg(short, long, default_value = "3000")]
42 port: u16,
43 },
44
45 Index {
47 #[arg(short, long)]
49 force: bool,
50
51 #[arg(long)]
53 check: bool,
54 },
55
56 Version,
58
59 Health,
61
62 Graph(GraphCommand),
64
65 Config(ConfigCommand),
67
68 #[cfg(feature = "vector-fastembed")]
70 Vectordb(VectordbCommand),
71}
72
73#[derive(Parser, Debug)]
75pub struct ConfigCommand {
76 #[command(subcommand)]
78 pub command: ConfigAction,
79}
80
81#[derive(Subcommand, Debug)]
83pub enum ConfigAction {
84 Path,
86
87 Get {
89 key: String,
91 },
92
93 Set {
95 key: String,
97
98 value: String,
100 },
101
102 Init {
104 #[arg(short, long)]
106 file: Option<String>,
107
108 #[arg(long)]
110 force: bool,
111 },
112
113 Export {
115 #[arg(long)]
117 docker_env: bool,
118 },
119}
120
121#[derive(Parser, Debug)]
123pub struct GraphCommand {
124 #[command(subcommand)]
126 pub command: GraphSubcommand,
127}
128
129#[derive(Subcommand, Debug)]
131pub enum GraphSubcommand {
132 Build {
134 #[arg(short, long)]
136 output: Option<String>,
137
138 #[arg(long)]
140 dry_run: bool,
141 },
142
143 Validate,
145
146 Stats,
148
149 Query {
151 #[arg(short, long)]
153 id: String,
154
155 #[arg(short = 't', long, default_value = "related")]
157 query_type: String,
158
159 #[arg(long)]
161 to: Option<String>,
162 },
163}
164
165#[cfg(feature = "vector-fastembed")]
171#[derive(Parser, Debug)]
172pub struct VectordbCommand {
173 #[command(subcommand)]
175 pub command: VectordbAction,
176}
177
178#[cfg(feature = "vector-fastembed")]
180#[derive(Subcommand, Debug)]
181pub enum VectordbAction {
182 GetModel {
184 #[arg(long)]
186 model: Option<String>,
187
188 #[arg(long)]
190 cache_dir: Option<String>,
191 },
192}
193
194pub trait CliExtension: Send + Sync {
203 type Command: Send + Sync;
205
206 fn handle_command(
208 &self,
209 command: Self::Command,
210 ) -> impl std::future::Future<Output = fabryk_core::Result<()>> + Send;
211}
212
213#[cfg(test)]
218mod tests {
219 use super::*;
220 use clap::Parser;
221
222 #[test]
223 fn test_cli_args_default() {
224 let args = CliArgs::parse_from(["test"]);
225 assert!(args.config.is_none());
226 assert!(!args.verbose);
227 assert!(!args.quiet);
228 assert!(args.command.is_none());
229 }
230
231 #[test]
232 fn test_cli_args_verbose() {
233 let args = CliArgs::parse_from(["test", "--verbose"]);
234 assert!(args.verbose);
235 assert!(!args.quiet);
236 }
237
238 #[test]
239 fn test_cli_args_quiet() {
240 let args = CliArgs::parse_from(["test", "--quiet"]);
241 assert!(!args.verbose);
242 assert!(args.quiet);
243 }
244
245 #[test]
246 fn test_cli_args_config() {
247 let args = CliArgs::parse_from(["test", "--config", "/path/to/config.toml"]);
248 assert_eq!(args.config, Some("/path/to/config.toml".to_string()));
249 }
250
251 #[test]
252 fn test_serve_command() {
253 let args = CliArgs::parse_from(["test", "serve"]);
254 match args.command {
255 Some(BaseCommand::Serve { port }) => assert_eq!(port, 3000),
256 _ => panic!("Expected Serve command"),
257 }
258 }
259
260 #[test]
261 fn test_serve_command_custom_port() {
262 let args = CliArgs::parse_from(["test", "serve", "--port", "8080"]);
263 match args.command {
264 Some(BaseCommand::Serve { port }) => assert_eq!(port, 8080),
265 _ => panic!("Expected Serve command"),
266 }
267 }
268
269 #[test]
270 fn test_index_command() {
271 let args = CliArgs::parse_from(["test", "index"]);
272 match args.command {
273 Some(BaseCommand::Index { force, check }) => {
274 assert!(!force);
275 assert!(!check);
276 }
277 _ => panic!("Expected Index command"),
278 }
279 }
280
281 #[test]
282 fn test_index_command_force() {
283 let args = CliArgs::parse_from(["test", "index", "--force"]);
284 match args.command {
285 Some(BaseCommand::Index { force, check }) => {
286 assert!(force);
287 assert!(!check);
288 }
289 _ => panic!("Expected Index command with force"),
290 }
291 }
292
293 #[test]
294 fn test_version_command() {
295 let args = CliArgs::parse_from(["test", "version"]);
296 assert!(matches!(args.command, Some(BaseCommand::Version)));
297 }
298
299 #[test]
300 fn test_health_command() {
301 let args = CliArgs::parse_from(["test", "health"]);
302 assert!(matches!(args.command, Some(BaseCommand::Health)));
303 }
304
305 #[test]
306 fn test_graph_build_command() {
307 let args = CliArgs::parse_from(["test", "graph", "build"]);
308 match args.command {
309 Some(BaseCommand::Graph(GraphCommand {
310 command: GraphSubcommand::Build { output, dry_run },
311 })) => {
312 assert!(output.is_none());
313 assert!(!dry_run);
314 }
315 _ => panic!("Expected Graph Build command"),
316 }
317 }
318
319 #[test]
320 fn test_graph_build_dry_run() {
321 let args = CliArgs::parse_from(["test", "graph", "build", "--dry-run"]);
322 match args.command {
323 Some(BaseCommand::Graph(GraphCommand {
324 command: GraphSubcommand::Build { dry_run, .. },
325 })) => {
326 assert!(dry_run);
327 }
328 _ => panic!("Expected Graph Build command with dry_run"),
329 }
330 }
331
332 #[test]
333 fn test_graph_validate_command() {
334 let args = CliArgs::parse_from(["test", "graph", "validate"]);
335 match args.command {
336 Some(BaseCommand::Graph(GraphCommand {
337 command: GraphSubcommand::Validate,
338 })) => {}
339 _ => panic!("Expected Graph Validate command"),
340 }
341 }
342
343 #[test]
344 fn test_graph_stats_command() {
345 let args = CliArgs::parse_from(["test", "graph", "stats"]);
346 match args.command {
347 Some(BaseCommand::Graph(GraphCommand {
348 command: GraphSubcommand::Stats,
349 })) => {}
350 _ => panic!("Expected Graph Stats command"),
351 }
352 }
353
354 #[test]
355 fn test_graph_query_command() {
356 let args = CliArgs::parse_from(["test", "graph", "query", "--id", "node-1"]);
357 match args.command {
358 Some(BaseCommand::Graph(GraphCommand {
359 command: GraphSubcommand::Query { id, query_type, to },
360 })) => {
361 assert_eq!(id, "node-1");
362 assert_eq!(query_type, "related");
363 assert!(to.is_none());
364 }
365 _ => panic!("Expected Graph Query command"),
366 }
367 }
368
369 #[test]
370 fn test_graph_query_path() {
371 let args = CliArgs::parse_from([
372 "test",
373 "graph",
374 "query",
375 "--id",
376 "a",
377 "--query-type",
378 "path",
379 "--to",
380 "b",
381 ]);
382 match args.command {
383 Some(BaseCommand::Graph(GraphCommand {
384 command: GraphSubcommand::Query { id, query_type, to },
385 })) => {
386 assert_eq!(id, "a");
387 assert_eq!(query_type, "path");
388 assert_eq!(to, Some("b".to_string()));
389 }
390 _ => panic!("Expected Graph Query path command"),
391 }
392 }
393
394 #[test]
399 fn test_config_path_command() {
400 let args = CliArgs::parse_from(["test", "config", "path"]);
401 match args.command {
402 Some(BaseCommand::Config(ConfigCommand {
403 command: ConfigAction::Path,
404 })) => {}
405 _ => panic!("Expected Config Path command"),
406 }
407 }
408
409 #[test]
410 fn test_config_get_command() {
411 let args = CliArgs::parse_from(["test", "config", "get", "server.port"]);
412 match args.command {
413 Some(BaseCommand::Config(ConfigCommand {
414 command: ConfigAction::Get { key },
415 })) => {
416 assert_eq!(key, "server.port");
417 }
418 _ => panic!("Expected Config Get command"),
419 }
420 }
421
422 #[test]
423 fn test_config_set_command() {
424 let args = CliArgs::parse_from(["test", "config", "set", "server.port", "8080"]);
425 match args.command {
426 Some(BaseCommand::Config(ConfigCommand {
427 command: ConfigAction::Set { key, value },
428 })) => {
429 assert_eq!(key, "server.port");
430 assert_eq!(value, "8080");
431 }
432 _ => panic!("Expected Config Set command"),
433 }
434 }
435
436 #[test]
437 fn test_config_init_command() {
438 let args = CliArgs::parse_from(["test", "config", "init"]);
439 match args.command {
440 Some(BaseCommand::Config(ConfigCommand {
441 command: ConfigAction::Init { file, force },
442 })) => {
443 assert!(file.is_none());
444 assert!(!force);
445 }
446 _ => panic!("Expected Config Init command"),
447 }
448 }
449
450 #[test]
451 fn test_config_init_force() {
452 let args = CliArgs::parse_from(["test", "config", "init", "--force"]);
453 match args.command {
454 Some(BaseCommand::Config(ConfigCommand {
455 command: ConfigAction::Init { force, .. },
456 })) => {
457 assert!(force);
458 }
459 _ => panic!("Expected Config Init command with force"),
460 }
461 }
462
463 #[test]
464 fn test_config_export_command() {
465 let args = CliArgs::parse_from(["test", "config", "export"]);
466 match args.command {
467 Some(BaseCommand::Config(ConfigCommand {
468 command: ConfigAction::Export { docker_env },
469 })) => {
470 assert!(!docker_env);
471 }
472 _ => panic!("Expected Config Export command"),
473 }
474 }
475
476 #[test]
477 fn test_config_export_docker_env() {
478 let args = CliArgs::parse_from(["test", "config", "export", "--docker-env"]);
479 match args.command {
480 Some(BaseCommand::Config(ConfigCommand {
481 command: ConfigAction::Export { docker_env },
482 })) => {
483 assert!(docker_env);
484 }
485 _ => panic!("Expected Config Export command with docker_env"),
486 }
487 }
488
489 #[cfg(feature = "vector-fastembed")]
494 #[test]
495 fn test_vectordb_get_model_command() {
496 use crate::cli::{VectordbAction, VectordbCommand};
497
498 let args = CliArgs::parse_from(["test", "vectordb", "get-model"]);
499 match args.command {
500 Some(BaseCommand::Vectordb(VectordbCommand {
501 command: VectordbAction::GetModel { model, cache_dir },
502 })) => {
503 assert!(model.is_none());
504 assert!(cache_dir.is_none());
505 }
506 _ => panic!("Expected Vectordb GetModel command"),
507 }
508 }
509
510 #[cfg(feature = "vector-fastembed")]
511 #[test]
512 fn test_vectordb_get_model_with_overrides() {
513 use crate::cli::{VectordbAction, VectordbCommand};
514
515 let args = CliArgs::parse_from([
516 "test",
517 "vectordb",
518 "get-model",
519 "--model",
520 "bge-large-en-v1.5",
521 "--cache-dir",
522 "/tmp/models",
523 ]);
524 match args.command {
525 Some(BaseCommand::Vectordb(VectordbCommand {
526 command: VectordbAction::GetModel { model, cache_dir },
527 })) => {
528 assert_eq!(model.as_deref(), Some("bge-large-en-v1.5"));
529 assert_eq!(cache_dir.as_deref(), Some("/tmp/models"));
530 }
531 _ => panic!("Expected Vectordb GetModel command with overrides"),
532 }
533 }
534}