use anyhow::Result;
use super::style::*;
use crate::{
cli::{ResetArgs, UpdateArgs, UpdateCommand},
config,
};
pub async fn cmd_reset(args: ResetArgs) -> Result<()> {
let base_dir = config::loader::base_dir();
let scope = args.scope.as_deref().unwrap_or("full");
if args.dry_run {
banner(&format!("rsclaw reset (dry run) v{}", option_env!("RSCLAW_BUILD_VERSION").unwrap_or("dev")));
match scope {
"config" => {
if let Some(path) = config::loader::detect_config_path() {
warn_msg(&format!("would remove config: {}", bold(&path.display().to_string())));
} else {
warn_msg("no config file found");
}
}
"full" => {
if base_dir.exists() {
warn_msg(&format!(
"would remove state dir: {}",
bold(&base_dir.display().to_string())
));
} else {
warn_msg(&format!(
"state dir not found: {}",
dim(&base_dir.display().to_string())
));
}
}
other => anyhow::bail!("unknown reset scope: {other} (use 'config' or 'full')"),
}
return Ok(());
}
banner(&format!("rsclaw reset v{}", option_env!("RSCLAW_BUILD_VERSION").unwrap_or("dev")));
println!(" {}", red("WARNING: This is a destructive operation!"));
println!();
match scope {
"config" => {
if let Some(path) = config::loader::detect_config_path() {
println!(" {} {}", red("removing"), bold(&path.display().to_string()));
std::fs::remove_file(&path)?;
ok(&format!("removed config: {}", dim(&path.display().to_string())));
} else {
warn_msg("no config file found");
}
}
"full" => {
if base_dir.exists() {
println!(" {} {}", red("removing"), bold(&base_dir.display().to_string()));
std::fs::remove_dir_all(&base_dir)?;
ok(&format!("removed state dir: {}", dim(&base_dir.display().to_string())));
} else {
warn_msg(&format!(
"state dir not found: {}",
dim(&base_dir.display().to_string())
));
}
}
other => anyhow::bail!("unknown reset scope: {other} (use 'config' or 'full')"),
}
Ok(())
}
pub async fn cmd_update(sub: UpdateCommand) -> Result<()> {
match sub {
UpdateCommand::Run(args) => do_update(&args).await?,
UpdateCommand::Status => update_status().await?,
UpdateCommand::Wizard => {
banner(&format!(
"rsclaw update wizard v{}",
option_env!("RSCLAW_BUILD_VERSION").unwrap_or("dev")
));
warn_msg("update wizard: not yet implemented");
println!(" {}", dim("use `rsclaw update run` for now"));
}
}
Ok(())
}
const RSCLAW_VERSION_URL: &str = "https://app.rsclaw.ai/api/version";
fn proxy_url(url: &str) -> String {
if let Ok(proxy) = std::env::var("GITHUB_PROXY") {
let proxy = proxy.trim_end_matches('/');
if !proxy.is_empty() {
return format!("{}/{}", proxy, url);
}
}
url.to_owned()
}
fn build_update_client(timeout_secs: u64) -> Result<reqwest::Client> {
Ok(reqwest::Client::builder()
.user_agent("rsclaw/dev")
.timeout(std::time::Duration::from_secs(timeout_secs))
.build()?)
}
async fn do_update(args: &UpdateArgs) -> Result<()> {
banner(&format!(
"rsclaw update v{}",
option_env!("RSCLAW_BUILD_VERSION").unwrap_or("dev")
));
let timeout_secs = args.timeout.unwrap_or(30);
let client = build_update_client(timeout_secs)?;
println!(" {} checking for updates...", dim("[..]"));
let release: serde_json::Value = {
let mut data = None;
let sources = [
RSCLAW_VERSION_URL.to_owned(),
proxy_url(&format!("https://api.github.com/repos/{}/releases?per_page=10", "rsclaw-ai/rsclaw")),
];
for url in &sources {
if let Ok(resp) = client.get(url).send().await {
if resp.status().is_success() {
if let Ok(arr) = resp.json::<Vec<serde_json::Value>>().await {
data = arr.into_iter().find(|r| {
r["tag_name"].as_str().is_some_and(|t| t.starts_with('v') && !t.starts_with("app-"))
});
if data.is_some() { break; }
}
}
}
}
data.unwrap_or_default()
};
let latest_version = release["tag_name"]
.as_str()
.unwrap_or("")
.trim_start_matches('v')
.to_owned();
let current = option_env!("RSCLAW_BUILD_VERSION").unwrap_or("dev");
kv("Current:", current);
kv("Latest:", &latest_version);
if latest_version.is_empty() {
println!(" {} could not determine latest version", yellow("[!]"));
return Ok(());
}
if latest_version == current {
println!(" {} already up to date", green("[ok]"));
return Ok(());
}
if args.json {
println!(
"{}",
serde_json::json!({
"currentVersion": current,
"latestVersion": latest_version,
"updateAvailable": true,
"dryRun": args.dry_run,
})
);
if args.dry_run {
return Ok(());
}
}
if args.dry_run {
println!(
" {} would update to {latest_version} (dry run)",
dim("[..]")
);
return Ok(());
}
let (os, arch) = (std::env::consts::OS, std::env::consts::ARCH);
let candidates: Vec<&str> = match (os, arch) {
("macos", "aarch64") => vec!["rsclaw-aarch64-apple-darwin"],
("macos", "x86_64") => vec!["rsclaw-x86_64-apple-darwin"],
("linux", "x86_64") => vec!["rsclaw-x86_64-unknown-linux-gnu", "rsclaw-x86_64-unknown-linux-musl"],
("linux", "aarch64") => vec!["rsclaw-aarch64-unknown-linux-gnu", "rsclaw-aarch64-unknown-linux-musl"],
("windows", "x86_64") => vec!["rsclaw-x86_64-pc-windows-msvc"],
("windows", "aarch64") => vec!["rsclaw-aarch64-pc-windows-msvc"],
_ => anyhow::bail!("unsupported platform: {os}-{arch}"),
};
let assets = release["assets"].as_array();
let mut asset_name = candidates[0];
let download_url = assets.and_then(|arr| {
for candidate in &candidates {
if let Some(url) = arr.iter().find_map(|a| {
let name = a["name"].as_str().unwrap_or("");
if name.contains(candidate) {
a["browser_download_url"].as_str().map(|s| s.to_owned())
} else {
None
}
}) {
asset_name = candidate;
return Some(url);
}
}
None
});
let Some(url) = download_url else {
println!(" {} no pre-built binary for {os}-{arch}", yellow("[!]"));
println!(" Update from source:");
println!(" cd /path/to/rsclaw && git pull && cargo build --release");
return Ok(());
};
println!(" {} downloading {asset_name}...", dim("[..]"));
let download = if url.contains("github.com") || url.contains("githubusercontent.com") { proxy_url(&url) } else { url.clone() };
let binary = client.get(&download).send().await?.bytes().await?;
if binary.is_empty() {
anyhow::bail!("downloaded binary is empty");
}
let current_exe = std::env::current_exe()?;
let backup = current_exe.with_extension("old");
std::fs::rename(¤t_exe, &backup)?;
std::fs::write(¤t_exe, &binary)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
¤t_exe,
std::fs::Permissions::from_mode(0o755),
)?;
}
println!(" {} updated to {latest_version}", green("[ok]"));
kv("Binary:", ¤t_exe.display().to_string());
kv("Backup:", &backup.display().to_string());
if !args.no_restart {
let pid_file = config::loader::pid_file();
if pid_file.exists() {
println!(" {} restarting gateway...", dim("[..]"));
if let Ok(pid_str) = std::fs::read_to_string(&pid_file) {
if let Ok(pid) = pid_str.trim().parse::<i32>() {
let _ = crate::sys::process_terminate(pid as u32);
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let _ = std::process::Command::new(¤t_exe)
.arg("gateway")
.arg("start")
.spawn();
println!(" {} gateway restarted", green("[ok]"));
}
}
}
}
println!();
Ok(())
}
async fn update_status() -> Result<()> {
banner(&format!(
"rsclaw update status v{}",
option_env!("RSCLAW_BUILD_VERSION").unwrap_or("dev")
));
let client = build_update_client(10)?;
kv("Current:", option_env!("RSCLAW_BUILD_VERSION").unwrap_or("dev"));
let find_cli_tag = |arr: &[serde_json::Value]| -> Option<String> {
arr.iter().find_map(|r| {
r["tag_name"].as_str().and_then(|t| {
if t.starts_with('v') && !t.starts_with("app-") { Some(t.to_owned()) } else { None }
})
})
};
let mut latest_tag: Option<String> = None;
let sources = [
RSCLAW_VERSION_URL.to_owned(),
proxy_url(&format!("https://api.github.com/repos/{}/releases?per_page=10", "rsclaw-ai/rsclaw")),
];
for url in &sources {
if let Ok(resp) = client.get(url).send().await {
if resp.status().is_success() {
if let Ok(arr) = resp.json::<Vec<serde_json::Value>>().await {
latest_tag = find_cli_tag(&arr);
if latest_tag.is_some() { break; }
}
}
}
}
match latest_tag {
Some(tag) => {
let latest = tag.trim_start_matches('v');
kv("Latest:", latest);
if latest == option_env!("RSCLAW_BUILD_VERSION").unwrap_or("dev") {
println!(" {} up to date", green("[ok]"));
} else {
println!(" {} update available: {latest}", yellow("[!]"));
println!(" Run: rsclaw update");
}
}
None => {
println!(" {} could not check for updates", yellow("[!]"));
}
}
println!();
Ok(())
}