mod client; mod credentials; mod interactive; mod jetstream; mod tui; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use std::io::{self, Write}; use client::AtProtoClient; use credentials::{CredentialStore, Credentials}; #[derive(Parser)] #[command(name = "thought")] #[command(about = "CLI tool for publishing stream.thought.blip records")] #[command(version = "0.1.0")] struct Cli { #[command(subcommand)] command: Option, /// Message to publish (if no subcommand is provided) message: Option, } #[derive(Subcommand)] enum Commands { /// Login to Bluesky with credentials Login, /// Logout and clear stored credentials Logout, /// Enter interactive mode for rapid posting Interactive { /// Use TUI interface with live message feed #[arg(long)] tui: bool, }, /// Enter the thought stream (TUI mode with live feed) Stream, } fn prompt_for_input(prompt: &str) -> Result { print!("{}", prompt); io::stdout().flush().context("Failed to flush stdout")?; let mut input = String::new(); io::stdin() .read_line(&mut input) .context("Failed to read input")?; Ok(input.trim().to_string()) } async fn handle_login() -> Result<()> { let store = CredentialStore::new()?; if store.exists() { println!("You are already logged in. Use 'thought logout' to clear credentials first."); return Ok(()); } println!("Login to Bluesky"); println!("================"); let username = prompt_for_input("Username (handle or email): ")?; if username.is_empty() { anyhow::bail!("Username cannot be empty"); } let password = rpassword::prompt_password("App Password: ") .context("Failed to read password")?; if password.is_empty() { anyhow::bail!("Password cannot be empty"); } let pds_uri = prompt_for_input("PDS URI (press Enter for https://bsky.social): ")?; let pds_uri = if pds_uri.is_empty() { "https://bsky.social".to_string() } else { pds_uri }; // Test the credentials by attempting to authenticate println!("Testing credentials..."); let credentials = Credentials::new(username, password, pds_uri); let mut client = AtProtoClient::new(&credentials.pds_uri); client.login(&credentials).await .context("Login failed. Please check your credentials.")?; // Store credentials if login was successful store.store(&credentials)?; Ok(()) } async fn handle_logout() -> Result<()> { let store = CredentialStore::new()?; store.clear()?; Ok(()) } async fn handle_publish(message: &str) -> Result<()> { let store = CredentialStore::new()?; let credentials = store.load()? .context("Not logged in. Please run 'thought login' first.")?; let mut client = AtProtoClient::new(&credentials.pds_uri); client.login(&credentials).await?; let uri = client.publish_blip(message).await?; println!("Published: {}", uri); Ok(()) } async fn handle_interactive(use_tui: bool) -> Result<()> { let store = CredentialStore::new()?; let credentials = store.load()? .context("Not logged in. Please run 'thought login' first.")?; let mut client = AtProtoClient::new(&credentials.pds_uri); client.login(&credentials).await?; interactive::run_interactive_mode(&client, use_tui).await?; Ok(()) } #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { Some(Commands::Login) => handle_login().await?, Some(Commands::Logout) => handle_logout().await?, Some(Commands::Interactive { tui }) => handle_interactive(tui).await?, Some(Commands::Stream) => handle_interactive(true).await?, None => { if let Some(message) = cli.message { handle_publish(&message).await?; } else { println!("Usage:"); println!(" thought login # Login to Bluesky"); println!(" thought logout # Logout and clear credentials"); println!(" thought interactive # Enter simple REPL mode"); println!(" thought stream # Enter TUI mode with live feed"); println!(" thought \"message\" # Publish a blip"); } } } Ok(()) }