use anyhow::Result;
use clap::{Parser, Subcommand};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use symbi_runtime::crypto::{Aes256GcmCrypto, KeyUtils};
use tempfile::NamedTempFile;
use tracing::{info, Level};
#[derive(Parser)]
#[command(name = "symbiont-mcp")]
#[command(about = "Symbiont MCP Server Management CLI")]
#[command(version = "0.1.0")]
struct Cli {
#[arg(short, long, default_value = "~/.symbiont/mcp-config.toml")]
config: PathBuf,
#[arg(short, long)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Add {
source: String,
#[arg(short, long)]
name: Option<String>,
#[arg(long)]
skip_verification: bool,
},
Remove {
name: String,
#[arg(short, long)]
force: bool,
},
List {
#[arg(short, long)]
detailed: bool,
#[arg(short, long)]
status: Option<String>,
},
Status {
name: Option<String>,
#[arg(short, long)]
health_check: bool,
},
Verify {
name: String,
#[arg(short, long)]
force: bool,
},
Update {
name: String,
#[arg(short, long)]
source: Option<String>,
},
Secrets {
#[command(subcommand)]
command: SecretsCommands,
},
}
#[derive(Subcommand)]
enum SecretsCommands {
Encrypt {
#[arg(long)]
r#in: PathBuf,
#[arg(long)]
out: PathBuf,
},
Decrypt {
#[arg(long)]
r#in: PathBuf,
},
Edit {
#[arg(long)]
file: PathBuf,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let level = if cli.verbose {
Level::DEBUG
} else {
Level::INFO
};
tracing_subscriber::fmt().with_max_level(level).init();
info!("Starting Symbiont MCP CLI");
match cli.command {
Commands::Add {
source,
name,
skip_verification,
} => {
println!("Add command not yet implemented");
println!(
"Source: {}, Name: {:?}, Skip verification: {}",
source, name, skip_verification
);
Ok(())
}
Commands::Remove { name, force } => {
println!("Remove command not yet implemented");
println!("Name: {}, Force: {}", name, force);
Ok(())
}
Commands::List { detailed, status } => {
println!("List command not yet implemented");
println!("Detailed: {}, Status: {:?}", detailed, status);
Ok(())
}
Commands::Status { name, health_check } => {
println!("Status command not yet implemented");
println!("Name: {:?}, Health check: {}", name, health_check);
Ok(())
}
Commands::Verify { name, force } => {
println!("Verify command not yet implemented");
println!("Name: {}, Force: {}", name, force);
Ok(())
}
Commands::Update { name, source } => {
println!("Update command not yet implemented");
println!("Name: {}, Source: {:?}", name, source);
Ok(())
}
Commands::Secrets { command } => handle_secrets_command(command).await,
}
}
async fn handle_secrets_command(command: SecretsCommands) -> Result<()> {
match command {
SecretsCommands::Encrypt { r#in, out } => encrypt_file(&r#in, &out).await,
SecretsCommands::Decrypt { r#in } => decrypt_file(&r#in).await,
SecretsCommands::Edit { file } => edit_encrypted_file(&file).await,
}
}
async fn encrypt_file(input_path: &PathBuf, output_path: &PathBuf) -> Result<()> {
let plaintext = fs::read_to_string(input_path).map_err(|e| {
anyhow::anyhow!(
"Failed to read input file '{}': {}",
input_path.display(),
e
)
})?;
serde_json::from_str::<serde_json::Value>(&plaintext)
.map_err(|e| anyhow::anyhow!("Input file is not valid JSON: {}", e))?;
let key_utils = KeyUtils::new();
let key = key_utils
.get_or_create_key()
.map_err(|e| anyhow::anyhow!("Failed to get encryption key: {}", e))?;
let crypto = Aes256GcmCrypto::new();
let encrypted_data = crypto
.encrypt(plaintext.as_bytes(), &key)
.map_err(|e| anyhow::anyhow!("Failed to encrypt data: {}", e))?;
fs::write(output_path, encrypted_data).map_err(|e| {
anyhow::anyhow!(
"Failed to write encrypted file '{}': {}",
output_path.display(),
e
)
})?;
println!(
"Successfully encrypted '{}' to '{}'",
input_path.display(),
output_path.display()
);
Ok(())
}
async fn decrypt_file(input_path: &PathBuf) -> Result<()> {
let encrypted_data = fs::read(input_path).map_err(|e| {
anyhow::anyhow!(
"Failed to read encrypted file '{}': {}",
input_path.display(),
e
)
})?;
let key_utils = KeyUtils::new();
let key = key_utils
.get_or_create_key()
.map_err(|e| anyhow::anyhow!("Failed to get decryption key: {}", e))?;
let crypto = Aes256GcmCrypto::new();
let decrypted_data = crypto
.decrypt(&encrypted_data, &key)
.map_err(|e| anyhow::anyhow!("Failed to decrypt data: {}", e))?;
let plaintext = String::from_utf8(decrypted_data)
.map_err(|e| anyhow::anyhow!("Decrypted data is not valid UTF-8: {}", e))?;
serde_json::from_str::<serde_json::Value>(&plaintext)
.map_err(|e| anyhow::anyhow!("Decrypted data is not valid JSON: {}", e))?;
print!("{}", plaintext);
Ok(())
}
async fn edit_encrypted_file(file_path: &PathBuf) -> Result<()> {
if !file_path.exists() {
return Err(anyhow::anyhow!(
"File '{}' does not exist",
file_path.display()
));
}
let editor = env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
let encrypted_data = fs::read(file_path).map_err(|e| {
anyhow::anyhow!(
"Failed to read encrypted file '{}': {}",
file_path.display(),
e
)
})?;
let key_utils = KeyUtils::new();
let key = key_utils
.get_or_create_key()
.map_err(|e| anyhow::anyhow!("Failed to get decryption key: {}", e))?;
let crypto = Aes256GcmCrypto::new();
let decrypted_data = crypto
.decrypt(&encrypted_data, &key)
.map_err(|e| anyhow::anyhow!("Failed to decrypt data: {}", e))?;
let plaintext = String::from_utf8(decrypted_data)
.map_err(|e| anyhow::anyhow!("Decrypted data is not valid UTF-8: {}", e))?;
serde_json::from_str::<serde_json::Value>(&plaintext)
.map_err(|e| anyhow::anyhow!("Decrypted data is not valid JSON: {}", e))?;
let temp_file = NamedTempFile::new()
.map_err(|e| anyhow::anyhow!("Failed to create temporary file: {}", e))?;
fs::write(temp_file.path(), &plaintext)
.map_err(|e| anyhow::anyhow!("Failed to write to temporary file: {}", e))?;
let status = Command::new(&editor)
.arg(temp_file.path())
.status()
.map_err(|e| anyhow::anyhow!("Failed to execute editor '{}': {}", editor, e))?;
if !status.success() {
return Err(anyhow::anyhow!(
"Editor '{}' exited with non-zero status",
editor
));
}
let modified_content = fs::read_to_string(temp_file.path()).map_err(|e| {
anyhow::anyhow!("Failed to read modified content from temporary file: {}", e)
})?;
serde_json::from_str::<serde_json::Value>(&modified_content)
.map_err(|e| anyhow::anyhow!("Modified content is not valid JSON: {}", e))?;
let encrypted_data = crypto
.encrypt(modified_content.as_bytes(), &key)
.map_err(|e| anyhow::anyhow!("Failed to re-encrypt data: {}", e))?;
fs::write(file_path, encrypted_data).map_err(|e| {
anyhow::anyhow!(
"Failed to write encrypted file '{}': {}",
file_path.display(),
e
)
})?;
println!(
"Successfully updated encrypted file '{}'",
file_path.display()
);
Ok(())
}