use log::{debug, error, info, warn};
use reqwest;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::Stdio;
use tempfile::NamedTempFile;
use tokio::fs as tokio_fs;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
pub async fn download_fabric_binary() -> Result<PathBuf, Box<dyn std::error::Error>> {
let cache_dir = Path::new("./data");
if let Err(e) = fs::create_dir_all(cache_dir) {
error!("Failed to create cache directory: {e}");
return Err(e.into());
} else {
debug!(
"Cache directory created or already exists at {:?}",
cache_dir
);
}
let platform_map = [
("windows", "fabric-windows-amd64.exe"),
(
"darwin",
if cfg!(target_arch = "aarch64") {
"fabric-darwin-arm64"
} else {
"fabric-darwin-amd64"
},
),
(
"linux",
if cfg!(target_arch = "aarch64") {
"fabric-linux-arm64"
} else {
"fabric-linux-amd64"
},
),
];
let platform = match env::consts::OS {
"windows" => "windows",
"macos" | "darwin" => "darwin",
"linux" => "linux",
_ => {
error!("Unsupported platform detected: {}", env::consts::OS);
return Err("Unsupported platform".into());
}
};
let binary_name_platform_specific = platform_map
.iter()
.find(|(p, _)| *p == platform)
.ok_or_else(|| {
error!("Failed to determine platform-specific binary name");
"Unsupported platform"
})?
.1;
let binary_path = cache_dir.join(binary_name_platform_specific);
if binary_path.exists() {
info!("Using cached binary at {:?}", binary_path);
return Ok(binary_path);
}
let url = format!(
"https://github.com/danielmiessler/fabric/releases/latest/download/{}",
binary_name_platform_specific
);
info!("Downloading fabric binary from: {}", url);
let temp_file = NamedTempFile::new()?;
let temp_path = temp_file.path().to_path_buf();
let response = reqwest::get(url).await?.error_for_status()?;
let content = response.bytes().await?;
let mut output_file = tokio_fs::File::create(&temp_path).await?;
output_file.write_all(&content).await?;
debug!("Downloaded binary to temporary path: {:?}", temp_path);
tokio_fs::rename(&temp_path, &binary_path).await?;
info!("Binary moved to cache directory: {:?}", binary_path);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = fs::metadata(&binary_path) {
let mut perms = metadata.permissions();
perms.set_mode(0o755);
if let Err(e) = fs::set_permissions(&binary_path, perms) {
warn!("Failed to set executable permissions: {}", e);
} else {
debug!("Set executable permissions for {:?}", binary_path);
}
}
}
Ok(binary_path)
}
pub async fn execute_fabric_command(args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
let binary_path = download_fabric_binary().await?;
info!("Executing fabric command with args: {:?}", args);
let output = Command::new(binary_path)
.args(args)
.output()
.expect("Failed to start fabric command");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
error!("Fabric command failed with error: {} {}", stderr, stdout);
}
info!("Fabric command output: {}", stdout);
Ok(stdout.to_string())
}
pub async fn fabric_setup() -> Result<(String, String, bool), Box<dyn std::error::Error>> {
let config_dir = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let env_path = PathBuf::from(format!("{}/.config/fabric/.env", config_dir));
if env_path.exists() {
info!("Found .env file at {:?}", env_path);
return parse_env_file(&env_path).await;
} else {
info!(".env file not found, running `fabric --setup` to configure the application");
let binary_path = download_fabric_binary().await?;
let mut cmd = tokio::process::Command::new(binary_path);
cmd.arg("--setup");
cmd.stdout(Stdio::piped());
let mut child = cmd.spawn().expect("failed to spawn command");
let stdout = child
.stdout
.take()
.expect("child did not have a handle to stdout");
let mut reader = BufReader::new(stdout).lines();
tokio::spawn(async move {
let status = child
.wait()
.await
.expect("child process encountered an error");
println!("child status was: {}", status);
});
while let Some(line) = reader.next_line().await? {
println!("{}", line);
}
}
if env_path.exists() {
info!(".env file created successfully at {:?}", env_path);
return parse_env_file(&env_path).await;
} else {
error!(".env file was not created after running `fabric --setup`");
return Err("Setup did not create the .env file".into());
}
}
async fn parse_env_file(
env_path: &PathBuf,
) -> Result<(String, String, bool), Box<dyn std::error::Error>> {
let content = fs::read_to_string(env_path)?;
let mut env_vars = HashMap::new();
for line in content.lines() {
if let Some((key, value)) = line.split_once('=') {
env_vars.insert(key.trim().to_string(), value.trim().to_string());
}
}
let default_vendor = env_vars
.get("DEFAULT_VENDOR")
.ok_or("DEFAULT_VENDOR not found in .env file")?
.clone();
let default_model = env_vars
.get("DEFAULT_MODEL")
.ok_or("DEFAULT_MODEL not found in .env file")?
.clone();
let youtube_api_key_defined = env_vars.contains_key("YOUTUBE_API_KEY");
debug!(
"DEFAULT_VENDOR: {}, DEFAULT_MODEL: {}, YOUTUBE_API_KEY defined: {}",
default_vendor, default_model, youtube_api_key_defined
);
Ok((default_vendor, default_model, youtube_api_key_defined))
}
pub async fn patterns() -> Result<String, Box<dyn std::error::Error>> {
let args = vec!["--listpatterns"];
let output = super::fabric::execute_fabric_command(&args).await?;
Ok(output)
}
pub async fn models() -> Result<String, Box<dyn std::error::Error>> {
let args = vec!["--listmodels"];
let output = super::fabric::execute_fabric_command(&args).await?;
Ok(output)
}
pub async fn change_default_model(model: &str) -> Result<String, Box<dyn std::error::Error>> {
let args = vec!["--changeDefaultModel", model];
let output = super::fabric::execute_fabric_command(&args).await?;
Ok(output)
}
pub async fn scrape(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let args = vec!["--scrape_url", url];
let output = super::fabric::execute_fabric_command(&args).await?;
Ok(output)
}