Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
585 changes: 519 additions & 66 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ categories = ["command-line-utilities", "network-programming"]
tokio = { version = "1.43", features = ["full"] }

# TLS and crypto
tokio-rustls = "0.26"
rustls = "0.23"
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"] }
rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12", "ring"] }
rustls-pemfile = "2"
rcgen = "0.13"
ed25519-dalek = { version = "2", features = ["rand_core"] }
Expand Down Expand Up @@ -110,6 +110,15 @@ zip = { version = "2", default-features = false, features = ["deflate"] }
# POSIX utilities (for Linux clipboard holder)
nix = { version = "0.29", features = ["process", "signal"] }

# HTTP client for version checking
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }

# Semantic versioning
semver = { version = "1", features = ["serde"] }

# TOML editing (preserve formatting during migrations)
toml_edit = "0.22"

# FFI (for future mobile bindings)
# uniffi = "0.28"

Expand Down
9 changes: 8 additions & 1 deletion crates/yoop-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ name = "yoop"
path = "src/main.rs"

[dependencies]
yoop-core = { path = "../yoop-core", features = ["full"] }
yoop-core = { path = "../yoop-core", features = ["mdns", "web"] }

# CLI
clap = { workspace = true }
Expand All @@ -41,12 +41,19 @@ serde_json = { workspace = true }
# UUID
uuid = { workspace = true }

# Time
chrono = { workspace = true }

# Clipboard (for internal holder command)
arboard = { workspace = true }
image = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }

[features]
default = ["update"]
update = ["yoop-core/update"]

[lints]
workspace = true
2 changes: 2 additions & 0 deletions crates/yoop-cli/src/commands/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ fn create_transfer_config(global_config: &Config) -> TransferConfig {

/// Run the clipboard command.
pub async fn run(args: ClipboardArgs) -> Result<()> {
super::spawn_update_check();

match args.action {
ClipboardAction::Share(share_args) => run_share(share_args, args.quiet, args.json).await,
ClipboardAction::Receive(recv_args) => run_receive(recv_args, args.quiet, args.json).await,
Expand Down
64 changes: 64 additions & 0 deletions crates/yoop-cli/src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ pub async fn run(args: ConfigArgs) -> Result<()> {
println!(" notifications = {}", config.ui.notifications);
println!(" sound = {}", config.ui.sound);
println!();

// [update]
println!("[update]");
println!(" auto_check = {}", config.update.auto_check);
println!(
" check_interval = \"{}s\"",
config.update.check_interval.as_secs()
);
println!(
" package_manager = {}",
config.update.package_manager.map_or_else(
|| "auto".to_string(),
|pm| format!("{:?}", pm).to_lowercase()
)
);
println!(" notify = {}", config.update.notify);
println!();
}

ConfigAction::List => {
Expand Down Expand Up @@ -198,6 +215,14 @@ pub async fn run(args: ConfigArgs) -> Result<()> {
println!(" ui.notifications Enable notifications (true/false)");
println!(" ui.sound Play sound on complete (true/false)");
println!();
println!("[update]");
println!(" update.auto_check Enable automatic update checks (true/false)");
println!(" update.check_interval Interval between checks (e.g., 24h, 7d)");
println!(
" update.package_manager Preferred package manager (npm, pnpm, yarn, bun, auto)"
);
println!(" update.notify Show update notifications (true/false)");
println!();
}

ConfigAction::Path => {
Expand Down Expand Up @@ -285,6 +310,15 @@ fn get_config_value(config: &yoop_core::config::Config, key: &str) -> Option<Str
"ui.notifications" => Some(config.ui.notifications.to_string()),
"ui.sound" => Some(config.ui.sound.to_string()),

// update
"update.auto_check" => Some(config.update.auto_check.to_string()),
"update.check_interval" => Some(format!("{}s", config.update.check_interval.as_secs())),
"update.package_manager" => Some(config.update.package_manager.map_or_else(
|| "auto".to_string(),
|pm| format!("{:?}", pm).to_lowercase(),
)),
"update.notify" => Some(config.update.notify.to_string()),

_ => None,
}
}
Expand Down Expand Up @@ -481,6 +515,36 @@ fn set_config_value(
Ok(true)
}

// update
"update.auto_check" => {
config.update.auto_check = value.parse()?;
Ok(true)
}
"update.check_interval" => {
config.update.check_interval = parse_duration(value)?;
Ok(true)
}
"update.package_manager" => {
if value == "auto" || value.is_empty() {
config.update.package_manager = None;
} else {
config.update.package_manager = Some(match value.to_lowercase().as_str() {
"npm" => yoop_core::config::PackageManagerKind::Npm,
"pnpm" => yoop_core::config::PackageManagerKind::Pnpm,
"yarn" => yoop_core::config::PackageManagerKind::Yarn,
"bun" => yoop_core::config::PackageManagerKind::Bun,
_ => {
anyhow::bail!("Invalid package manager. Use: npm, pnpm, yarn, bun, or auto")
}
});
}
Ok(true)
}
"update.notify" => {
config.update.notify = value.parse()?;
Ok(true)
}

_ => Ok(false),
}
}
Expand Down
78 changes: 78 additions & 0 deletions crates/yoop-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,49 @@ pub fn load_config() -> yoop_core::config::Config {
yoop_core::config::Config::load().unwrap_or_default()
}

/// Spawn a background task to check for updates.
///
/// This function spawns a non-blocking background task that:
/// - Checks if auto_check and notify are enabled in config
/// - Respects the check_interval to avoid excessive API calls
/// - Displays a message to stderr if an update is available
/// - Silently ignores errors and no-update cases
///
/// This should be called by long-running commands (share, receive, clipboard).
#[cfg(feature = "update")]
pub fn spawn_update_check() {
use yoop_core::config::Config;
use yoop_core::update::version_check::VersionChecker;

tokio::spawn(async move {
let Ok(mut config) = Config::load() else {
return;
};

if !config.update.auto_check || !config.update.notify {
return;
}

let checker = VersionChecker::new();

match checker.check_with_cache(&mut config).await {
Ok(Some(status)) if status.update_available => {
eprintln!();
eprintln!(
" Update available: {} -> {}",
status.current_version, status.latest_version
);
eprintln!(" Run 'yoop update' to upgrade.");
eprintln!();
}
_ => {}
}
});
}

#[cfg(not(feature = "update"))]
pub fn spawn_update_check() {}

pub mod clipboard;
pub mod completions;
pub mod config;
Expand All @@ -22,6 +65,8 @@ pub mod scan;
pub mod send;
pub mod share;
pub mod trust;
#[cfg(feature = "update")]
pub mod update;
pub mod web;

/// Yoop - Cross-platform local network file sharing
Expand Down Expand Up @@ -71,6 +116,10 @@ pub enum Command {
/// Generate shell completions
Completions(CompletionsArgs),

/// Check for and install updates
#[cfg(feature = "update")]
Update(UpdateArgs),

/// Internal: hold clipboard content (not user-facing, used by spawn)
#[command(hide = true)]
InternalClipboardHold(InternalClipboardHoldArgs),
Expand Down Expand Up @@ -415,3 +464,32 @@ pub struct InternalClipboardHoldArgs {
#[arg(long, default_value = "300")]
pub timeout: u64,
}

/// Arguments for the update command
#[cfg(feature = "update")]
#[derive(Parser)]
pub struct UpdateArgs {
/// Only check for updates, don't install
#[arg(long)]
pub check: bool,

/// Rollback to previous version (restores backup)
#[arg(long)]
pub rollback: bool,

/// Force update even if already on latest version
#[arg(long)]
pub force: bool,

/// Specify package manager: npm, pnpm, yarn, or bun
#[arg(long)]
pub package_manager: Option<String>,

/// Output in JSON format
#[arg(long)]
pub json: bool,

/// Minimal output
#[arg(short, long)]
pub quiet: bool,
}
2 changes: 2 additions & 0 deletions crates/yoop-cli/src/commands/receive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use super::ReceiveArgs;
pub async fn run(args: ReceiveArgs) -> Result<()> {
let global_config = super::load_config();

super::spawn_update_check();

let code = yoop_core::code::ShareCode::parse(&args.code)?;

if !args.quiet && !args.json {
Expand Down
2 changes: 2 additions & 0 deletions crates/yoop-cli/src/commands/share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use crate::ui::{format_remaining, parse_duration, CodeBox};
pub async fn run(args: ShareArgs) -> Result<()> {
let global_config = super::load_config();

super::spawn_update_check();

let compress =
args.compress || matches!(global_config.transfer.compression, CompressionMode::Always);

Expand Down
Loading