faucet_cli/commands/
install.rs1use crate::cli::InstallArgs;
8use crate::error::{CliError, CliResult};
9use crate::registry::{sink_exists, source_exists};
10use crate::registry_index::{InstallRecipe, RegistryIndex, install_recipe};
11
12pub async fn run(args: InstallArgs) -> CliResult<()> {
14 let idx = RegistryIndex::load(args.index.as_deref())?;
15 let matches = idx.find(&args.name, args.kind.as_deref());
16 let entry = match matches.as_slice() {
17 [] => {
18 return Err(CliError::Config(format!(
19 "connector '{}' is not in the registry index — try `faucet search {}`",
20 args.name, args.name
21 )));
22 }
23 [one] => *one,
24 many => {
25 return Err(CliError::Config(format!(
26 "connector '{}' is ambiguous ({} entries: {}); disambiguate with --kind source|sink",
27 args.name,
28 many.len(),
29 many.iter()
30 .map(|c| c.kind.as_str())
31 .collect::<Vec<_>>()
32 .join(", ")
33 )));
34 }
35 };
36
37 let compiled = match entry.kind.as_str() {
38 "source" => source_exists(&entry.name),
39 "sink" => sink_exists(&entry.name),
40 _ => false,
41 };
42
43 match install_recipe(entry, compiled) {
44 InstallRecipe::AlreadyAvailable { feature } => {
45 println!(
46 "✔ {} '{}' is already available in this binary (feature `{}`).",
47 entry.kind, entry.name, feature
48 );
49 println!(" Use it directly: `type: {}` in your config.", entry.name);
50 }
51 InstallRecipe::CargoInstall { feature } => {
52 println!(
53 "'{}' is a built-in {} connector, not compiled into this binary.",
54 entry.name, entry.kind
55 );
56 println!("Reinstall the CLI with it enabled:\n");
57 println!(" cargo install faucet-cli --features {feature}\n");
58 println!("(add `{feature}` to your existing `--features` list to keep the others).");
59 }
60 InstallRecipe::CustomBinary { krate, feature } => {
61 println!(
62 "'{}' is a community {} connector (crate `{}`).",
63 entry.name, entry.kind, krate
64 );
65 println!("Use it by building a custom `faucet` binary that registers it:\n");
66 println!(" cargo new my-faucet && cd my-faucet");
67 println!(" cargo add faucet-cli faucet-core {krate}\n");
68 let (reg_fn, trait_ctor) = match entry.kind.as_str() {
69 "source" => ("register_source", "MySource::from_value(cfg)?"),
70 _ => ("register_sink", "MySink::from_value(cfg)?"),
71 };
72 println!(" // src/main.rs");
73 println!(" use faucet_cli::registry::PluginRegistry;");
74 println!(" fn main() -> std::process::ExitCode {{");
75 println!(
76 " let reg = PluginRegistry::with_builtins().{reg_fn}(\"{}\", |cfg| Ok(Box::new({trait_ctor})));",
77 entry.name
78 );
79 println!(" faucet_cli::run_main(reg)");
80 println!(" }}\n");
81 println!(
82 "See cli/README.md → \"Custom binaries with third-party connectors\". (feature hint: `{feature}`)"
83 );
84 }
85 }
86 Ok(())
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::cli::InstallArgs;
93
94 #[tokio::test]
95 async fn install_builtin_ok() {
96 run(InstallArgs {
98 name: "jsonl".into(),
99 kind: Some("sink".into()),
100 index: None,
101 })
102 .await
103 .unwrap();
104 }
105
106 #[tokio::test]
107 async fn install_unknown_errors() {
108 let err = run(InstallArgs {
109 name: "nope-connector".into(),
110 kind: None,
111 index: None,
112 })
113 .await
114 .unwrap_err();
115 assert!(matches!(err, CliError::Config(_)));
116 }
117
118 #[tokio::test]
119 async fn install_ambiguous_requires_kind() {
120 let err = run(InstallArgs {
122 name: "postgres".into(),
123 kind: None,
124 index: None,
125 })
126 .await
127 .unwrap_err();
128 match err {
129 CliError::Config(msg) => assert!(msg.contains("ambiguous"), "{msg}"),
130 other => panic!("expected ambiguity error, got {other:?}"),
131 }
132 }
133
134 #[tokio::test]
135 async fn install_community_custom_binary() {
136 let dir = tempfile::tempdir().unwrap();
137 let p = dir.path().join("idx.json");
138 std::fs::write(
139 &p,
140 r#"{"version":1,"connectors":[{"name":"acme","kind":"source","verified":false,"description":"Acme"}]}"#,
141 )
142 .unwrap();
143 run(InstallArgs {
144 name: "acme".into(),
145 kind: None,
146 index: Some(p),
147 })
148 .await
149 .unwrap();
150 }
151}