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: Option<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 #[arg(long)]
121 file: Option<String>,
122 },
123}
124
125#[derive(Parser, Debug)]
127pub struct GraphCommand {
128 #[command(subcommand)]
130 pub command: GraphSubcommand,
131}
132
133#[derive(Subcommand, Debug)]
135pub enum GraphSubcommand {
136 Build {
138 #[arg(short, long)]
140 output: Option<String>,
141
142 #[arg(long)]
144 dry_run: bool,
145 },
146
147 Validate,
149
150 Stats,
152
153 Query {
155 #[arg(short, long)]
157 id: String,
158
159 #[arg(short = 't', long, default_value = "related")]
161 query_type: String,
162
163 #[arg(long)]
165 to: Option<String>,
166 },
167}
168
169#[cfg(feature = "vector-fastembed")]
175#[derive(Parser, Debug)]
176pub struct VectordbCommand {
177 #[command(subcommand)]
179 pub command: VectordbAction,
180}
181
182#[cfg(feature = "vector-fastembed")]
184#[derive(Subcommand, Debug)]
185pub enum VectordbAction {
186 GetModel {
188 #[arg(long)]
190 model: Option<String>,
191
192 #[arg(long)]
194 cache_dir: Option<String>,
195 },
196}
197
198pub trait CliExtension: Send + Sync {
207 type Command: Send + Sync;
209
210 fn handle_command(
212 &self,
213 command: Self::Command,
214 ) -> impl std::future::Future<Output = fabryk_core::Result<()>> + Send;
215}
216
217#[cfg(test)]
222mod tests {
223 use super::*;
224 use clap::Parser;
225
226 #[test]
227 fn test_cli_args_default() {
228 let args = CliArgs::parse_from(["test"]);
229 assert!(args.config.is_none());
230 assert!(!args.verbose);
231 assert!(!args.quiet);
232 assert!(args.command.is_none());
233 }
234
235 #[test]
236 fn test_cli_args_verbose() {
237 let args = CliArgs::parse_from(["test", "--verbose"]);
238 assert!(args.verbose);
239 assert!(!args.quiet);
240 }
241
242 #[test]
243 fn test_cli_args_quiet() {
244 let args = CliArgs::parse_from(["test", "--quiet"]);
245 assert!(!args.verbose);
246 assert!(args.quiet);
247 }
248
249 #[test]
250 fn test_cli_args_config() {
251 let args = CliArgs::parse_from(["test", "--config", "/path/to/config.toml"]);
252 assert_eq!(args.config, Some("/path/to/config.toml".to_string()));
253 }
254
255 #[test]
256 fn test_serve_command() {
257 let args = CliArgs::parse_from(["test", "serve"]);
258 match args.command {
259 Some(BaseCommand::Serve { port }) => assert_eq!(port, 3000),
260 _ => panic!("Expected Serve command"),
261 }
262 }
263
264 #[test]
265 fn test_serve_command_custom_port() {
266 let args = CliArgs::parse_from(["test", "serve", "--port", "8080"]);
267 match args.command {
268 Some(BaseCommand::Serve { port }) => assert_eq!(port, 8080),
269 _ => panic!("Expected Serve command"),
270 }
271 }
272
273 #[test]
274 fn test_index_command() {
275 let args = CliArgs::parse_from(["test", "index"]);
276 match args.command {
277 Some(BaseCommand::Index { force, check }) => {
278 assert!(!force);
279 assert!(!check);
280 }
281 _ => panic!("Expected Index command"),
282 }
283 }
284
285 #[test]
286 fn test_index_command_force() {
287 let args = CliArgs::parse_from(["test", "index", "--force"]);
288 match args.command {
289 Some(BaseCommand::Index { force, check }) => {
290 assert!(force);
291 assert!(!check);
292 }
293 _ => panic!("Expected Index command with force"),
294 }
295 }
296
297 #[test]
298 fn test_version_command() {
299 let args = CliArgs::parse_from(["test", "version"]);
300 assert!(matches!(args.command, Some(BaseCommand::Version)));
301 }
302
303 #[test]
304 fn test_health_command() {
305 let args = CliArgs::parse_from(["test", "health"]);
306 assert!(matches!(args.command, Some(BaseCommand::Health)));
307 }
308
309 #[test]
310 fn test_graph_build_command() {
311 let args = CliArgs::parse_from(["test", "graph", "build"]);
312 match args.command {
313 Some(BaseCommand::Graph(GraphCommand {
314 command: GraphSubcommand::Build { output, dry_run },
315 })) => {
316 assert!(output.is_none());
317 assert!(!dry_run);
318 }
319 _ => panic!("Expected Graph Build command"),
320 }
321 }
322
323 #[test]
324 fn test_graph_build_dry_run() {
325 let args = CliArgs::parse_from(["test", "graph", "build", "--dry-run"]);
326 match args.command {
327 Some(BaseCommand::Graph(GraphCommand {
328 command: GraphSubcommand::Build { dry_run, .. },
329 })) => {
330 assert!(dry_run);
331 }
332 _ => panic!("Expected Graph Build command with dry_run"),
333 }
334 }
335
336 #[test]
337 fn test_graph_validate_command() {
338 let args = CliArgs::parse_from(["test", "graph", "validate"]);
339 match args.command {
340 Some(BaseCommand::Graph(GraphCommand {
341 command: GraphSubcommand::Validate,
342 })) => {}
343 _ => panic!("Expected Graph Validate command"),
344 }
345 }
346
347 #[test]
348 fn test_graph_stats_command() {
349 let args = CliArgs::parse_from(["test", "graph", "stats"]);
350 match args.command {
351 Some(BaseCommand::Graph(GraphCommand {
352 command: GraphSubcommand::Stats,
353 })) => {}
354 _ => panic!("Expected Graph Stats command"),
355 }
356 }
357
358 #[test]
359 fn test_graph_query_command() {
360 let args = CliArgs::parse_from(["test", "graph", "query", "--id", "node-1"]);
361 match args.command {
362 Some(BaseCommand::Graph(GraphCommand {
363 command: GraphSubcommand::Query { id, query_type, to },
364 })) => {
365 assert_eq!(id, "node-1");
366 assert_eq!(query_type, "related");
367 assert!(to.is_none());
368 }
369 _ => panic!("Expected Graph Query command"),
370 }
371 }
372
373 #[test]
374 fn test_graph_query_path() {
375 let args = CliArgs::parse_from([
376 "test",
377 "graph",
378 "query",
379 "--id",
380 "a",
381 "--query-type",
382 "path",
383 "--to",
384 "b",
385 ]);
386 match args.command {
387 Some(BaseCommand::Graph(GraphCommand {
388 command: GraphSubcommand::Query { id, query_type, to },
389 })) => {
390 assert_eq!(id, "a");
391 assert_eq!(query_type, "path");
392 assert_eq!(to, Some("b".to_string()));
393 }
394 _ => panic!("Expected Graph Query path command"),
395 }
396 }
397
398 #[test]
403 fn test_config_path_command() {
404 let args = CliArgs::parse_from(["test", "config", "path"]);
405 match args.command {
406 Some(BaseCommand::Config(ConfigCommand {
407 command: ConfigAction::Path,
408 })) => {}
409 _ => panic!("Expected Config Path command"),
410 }
411 }
412
413 #[test]
414 fn test_config_get_command() {
415 let args = CliArgs::parse_from(["test", "config", "get", "server.port"]);
416 match args.command {
417 Some(BaseCommand::Config(ConfigCommand {
418 command: ConfigAction::Get { key },
419 })) => {
420 assert_eq!(key, Some("server.port".to_string()));
421 }
422 _ => panic!("Expected Config Get command"),
423 }
424 }
425
426 #[test]
427 fn test_config_get_no_key_dumps_all() {
428 let args = CliArgs::parse_from(["test", "config", "get"]);
429 match args.command {
430 Some(BaseCommand::Config(ConfigCommand {
431 command: ConfigAction::Get { key },
432 })) => {
433 assert!(key.is_none());
434 }
435 _ => panic!("Expected Config Get command"),
436 }
437 }
438
439 #[test]
440 fn test_config_set_command() {
441 let args = CliArgs::parse_from(["test", "config", "set", "server.port", "8080"]);
442 match args.command {
443 Some(BaseCommand::Config(ConfigCommand {
444 command: ConfigAction::Set { key, value },
445 })) => {
446 assert_eq!(key, "server.port");
447 assert_eq!(value, "8080");
448 }
449 _ => panic!("Expected Config Set command"),
450 }
451 }
452
453 #[test]
454 fn test_config_init_command() {
455 let args = CliArgs::parse_from(["test", "config", "init"]);
456 match args.command {
457 Some(BaseCommand::Config(ConfigCommand {
458 command: ConfigAction::Init { file, force },
459 })) => {
460 assert!(file.is_none());
461 assert!(!force);
462 }
463 _ => panic!("Expected Config Init command"),
464 }
465 }
466
467 #[test]
468 fn test_config_init_force() {
469 let args = CliArgs::parse_from(["test", "config", "init", "--force"]);
470 match args.command {
471 Some(BaseCommand::Config(ConfigCommand {
472 command: ConfigAction::Init { force, .. },
473 })) => {
474 assert!(force);
475 }
476 _ => panic!("Expected Config Init command with force"),
477 }
478 }
479
480 #[test]
481 fn test_config_export_command() {
482 let args = CliArgs::parse_from(["test", "config", "export"]);
483 match args.command {
484 Some(BaseCommand::Config(ConfigCommand {
485 command: ConfigAction::Export { docker_env, file },
486 })) => {
487 assert!(!docker_env);
488 assert!(file.is_none());
489 }
490 _ => panic!("Expected Config Export command"),
491 }
492 }
493
494 #[test]
495 fn test_config_export_docker_env() {
496 let args = CliArgs::parse_from(["test", "config", "export", "--docker-env"]);
497 match args.command {
498 Some(BaseCommand::Config(ConfigCommand {
499 command: ConfigAction::Export { docker_env, file },
500 })) => {
501 assert!(docker_env);
502 assert!(file.is_none());
503 }
504 _ => panic!("Expected Config Export command with docker_env"),
505 }
506 }
507
508 #[cfg(feature = "vector-fastembed")]
513 #[test]
514 fn test_vectordb_get_model_command() {
515 use crate::cli::{VectordbAction, VectordbCommand};
516
517 let args = CliArgs::parse_from(["test", "vectordb", "get-model"]);
518 match args.command {
519 Some(BaseCommand::Vectordb(VectordbCommand {
520 command: VectordbAction::GetModel { model, cache_dir },
521 })) => {
522 assert!(model.is_none());
523 assert!(cache_dir.is_none());
524 }
525 _ => panic!("Expected Vectordb GetModel command"),
526 }
527 }
528
529 #[cfg(feature = "vector-fastembed")]
530 #[test]
531 fn test_vectordb_get_model_with_overrides() {
532 use crate::cli::{VectordbAction, VectordbCommand};
533
534 let args = CliArgs::parse_from([
535 "test",
536 "vectordb",
537 "get-model",
538 "--model",
539 "bge-large-en-v1.5",
540 "--cache-dir",
541 "/tmp/models",
542 ]);
543 match args.command {
544 Some(BaseCommand::Vectordb(VectordbCommand {
545 command: VectordbAction::GetModel { model, cache_dir },
546 })) => {
547 assert_eq!(model.as_deref(), Some("bge-large-en-v1.5"));
548 assert_eq!(cache_dir.as_deref(), Some("/tmp/models"));
549 }
550 _ => panic!("Expected Vectordb GetModel command with overrides"),
551 }
552 }
553}