Skip to content

Commit 46846a5

Browse files
committed
feat(cli): implement upgrade command
- Add upgrade command to check for and download latest releases from GitHub - Compare current version with latest release, skip if already up to date - Download new binaries for all configured platforms - Update init command to download real binaries from GitHub releases - Add git repo root check to init (errors if not at root) - Add serde_json dependency for GitHub API parsing - Add rustls-tls and json features to reqwest Closes #11
1 parent 7f65d74 commit 46846a5

File tree

3 files changed

+230
-39
lines changed

3 files changed

+230
-39
lines changed

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ dialoguer = "0.11"
2828
console = "0.15"
2929

3030
# HTTP client for init/upgrade
31-
reqwest = { version = "0.12", features = ["blocking"], default-features = false, optional = true }
31+
reqwest = { version = "0.12", features = ["blocking", "rustls-tls", "json"], default-features = false, optional = true }
32+
33+
# JSON parsing for GitHub API
34+
serde_json = "1"
3235

3336
# Async runtime for parallel execution
3437
tokio = { version = "1", features = ["rt-multi-thread", "process", "sync"], optional = true }

src/commands/init.rs

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ pub fn run(args: &InitArgs) -> Result<()> {
3838
return Ok(());
3939
}
4040

41+
// Error if not at git repo root
42+
if !is_git_repo_root()? {
43+
bail!(
44+
"This directory does not appear to be a git repository root.\n\
45+
rnr must be initialized at the root of a git repository.\n\
46+
Please run 'rnr init' from the directory containing your .git folder."
47+
);
48+
}
49+
4150
// Determine platforms to install
4251
let platforms = select_platforms(args)?;
4352

@@ -49,6 +58,13 @@ pub fn run(args: &InitArgs) -> Result<()> {
4958
initialize(&platforms)
5059
}
5160

61+
/// Check if the current directory is a git repository root
62+
fn is_git_repo_root() -> Result<bool> {
63+
let current_dir = std::env::current_dir().context("Failed to get current directory")?;
64+
let git_dir = current_dir.join(".git");
65+
Ok(git_dir.exists())
66+
}
67+
5268
/// Select platforms based on args or interactively
5369
fn select_platforms(args: &InitArgs) -> Result<Vec<Platform>> {
5470
// --all-platforms
@@ -191,28 +207,42 @@ fn download_binaries(platforms: &[Platform], bin_directory: &Path) -> Result<()>
191207
Ok(())
192208
}
193209

210+
/// GitHub repository for releases
211+
const GITHUB_REPO: &str = "CodingWithCalvin/rnr.cli";
212+
194213
/// Download a single binary from GitHub releases
195214
#[cfg(feature = "network")]
196215
fn download_binary(platform: Platform, dest: &Path) -> Result<()> {
197-
use std::io::Write;
198-
199-
// TODO: Replace with actual release URL once we have releases
200-
// For now, use GitHub releases URL pattern
201216
let url = format!(
202-
"https://github.com/CodingWithCalvin/rnr.cli/releases/latest/download/{}",
217+
"https://github.com/{}/releases/latest/download/{}",
218+
GITHUB_REPO,
203219
platform.binary_name()
204220
);
205221

206-
// For now, create a placeholder since we don't have releases yet
207-
// In production, this would download from the URL
208-
let placeholder = format!(
209-
"#!/bin/sh\necho 'Placeholder binary for {}. Replace with actual binary from releases.'\n",
210-
platform.id()
211-
);
222+
let client = reqwest::blocking::Client::builder()
223+
.user_agent("rnr-cli")
224+
.build()
225+
.context("Failed to create HTTP client")?;
212226

213-
let mut file =
214-
fs::File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
215-
file.write_all(placeholder.as_bytes())?;
227+
let response = client
228+
.get(&url)
229+
.send()
230+
.with_context(|| format!("Failed to download {}", platform.binary_name()))?;
231+
232+
if !response.status().is_success() {
233+
anyhow::bail!(
234+
"Failed to download {}: HTTP {}",
235+
platform.binary_name(),
236+
response.status().as_u16()
237+
);
238+
}
239+
240+
let bytes = response
241+
.bytes()
242+
.with_context(|| format!("Failed to read response for {}", platform.binary_name()))?;
243+
244+
// Write to file
245+
fs::write(dest, &bytes).with_context(|| format!("Failed to write {}", dest.display()))?;
216246

217247
// Make executable on Unix
218248
#[cfg(unix)]
@@ -223,14 +253,6 @@ fn download_binary(platform: Platform, dest: &Path) -> Result<()> {
223253
fs::set_permissions(dest, perms)?;
224254
}
225255

226-
// TODO: Actual download implementation:
227-
// let response = reqwest::blocking::get(&url)
228-
// .with_context(|| format!("Failed to download {}", url))?;
229-
// let bytes = response.bytes()?;
230-
// fs::write(dest, bytes)?;
231-
232-
let _ = url; // Suppress unused warning for now
233-
234256
Ok(())
235257
}
236258

src/commands/upgrade.rs

Lines changed: 182 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
//! Upgrade rnr binaries to the latest version
2+
13
use anyhow::{Context, Result};
4+
use std::fs;
25
use std::path::PathBuf;
36

7+
use crate::platform::Platform;
8+
use crate::rnr_config::RnrConfig;
9+
10+
/// GitHub repository for releases
11+
const GITHUB_REPO: &str = "CodingWithCalvin/rnr.cli";
12+
413
/// Run the upgrade command
514
pub fn run() -> Result<()> {
615
let rnr_dir = find_rnr_dir()?;
@@ -10,16 +19,26 @@ pub fn run() -> Result<()> {
1019
anyhow::bail!("rnr is not initialized. Run 'rnr init' first.");
1120
}
1221

13-
println!("Upgrading rnr binaries...");
22+
// Load current config
23+
let config_path = rnr_dir.join("config.yaml");
24+
let mut config = RnrConfig::load_from(&config_path)?;
25+
let platforms = config.get_platforms();
26+
27+
if platforms.is_empty() {
28+
anyhow::bail!("No platforms configured. Run 'rnr init' to set up platforms.");
29+
}
30+
31+
println!("Checking for updates...\n");
32+
println!(" Current version: v{}", config.version);
1433

1534
#[cfg(feature = "network")]
1635
{
17-
upgrade_binaries(&bin_dir)?;
36+
upgrade_binaries(&bin_dir, &mut config, &config_path, &platforms)?;
1837
}
1938

2039
#[cfg(not(feature = "network"))]
2140
{
22-
println!("Network feature is disabled. Cannot download updates.");
41+
println!("\nNetwork feature is disabled. Cannot check for updates.");
2342
println!("Please manually update binaries in .rnr/bin/");
2443
}
2544

@@ -46,22 +65,169 @@ fn find_rnr_dir() -> Result<PathBuf> {
4665
anyhow::bail!("No .rnr directory found. Run 'rnr init' first.")
4766
}
4867

49-
/// Download and replace binaries
68+
/// Upgrade binaries to the latest version
5069
#[cfg(feature = "network")]
51-
fn upgrade_binaries(bin_dir: &std::path::Path) -> Result<()> {
52-
// TODO: Implement actual binary downloads
53-
// 1. Check current version
54-
// 2. Check latest version from server
55-
// 3. Download if newer version available
56-
// 4. Replace binaries
70+
fn upgrade_binaries(
71+
bin_dir: &std::path::Path,
72+
config: &mut RnrConfig,
73+
config_path: &std::path::Path,
74+
platforms: &[Platform],
75+
) -> Result<()> {
76+
// Get latest release info from GitHub
77+
let latest_version = get_latest_version()?;
78+
println!(" Latest version: v{}", latest_version);
5779

58-
println!(" Checking for updates...");
59-
println!(" TODO: Check https://rnr.dev/bin/latest/");
60-
println!(" TODO: Download updated binaries");
61-
println!("\nUpgrade complete!");
80+
// Compare versions
81+
if !is_newer_version(&config.version, &latest_version) {
82+
println!("\nYou're already on the latest version!");
83+
return Ok(());
84+
}
6285

63-
// Placeholder for actual implementation
64-
let _ = bin_dir;
86+
println!("\nUpgrading to v{}...\n", latest_version);
87+
88+
// Download new binaries for all configured platforms
89+
for platform in platforms {
90+
print!(" Downloading {}...", platform.binary_name());
91+
let binary_path = bin_dir.join(platform.binary_name());
92+
download_binary(*platform, &latest_version, &binary_path)?;
93+
println!(" done");
94+
}
95+
96+
// Update config version
97+
config.version = latest_version.clone();
98+
config.save_to(config_path)?;
99+
100+
println!("\nUpgrade complete! Now running v{}", latest_version);
65101

66102
Ok(())
67103
}
104+
105+
/// Get the latest release version from GitHub
106+
#[cfg(feature = "network")]
107+
fn get_latest_version() -> Result<String> {
108+
let url = format!(
109+
"https://api.github.com/repos/{}/releases/latest",
110+
GITHUB_REPO
111+
);
112+
113+
let client = reqwest::blocking::Client::builder()
114+
.user_agent("rnr-cli")
115+
.build()
116+
.context("Failed to create HTTP client")?;
117+
118+
let response = client
119+
.get(&url)
120+
.send()
121+
.context("Failed to fetch latest release info")?;
122+
123+
if !response.status().is_success() {
124+
if response.status().as_u16() == 404 {
125+
anyhow::bail!("No releases found. This may be the first version.");
126+
}
127+
anyhow::bail!(
128+
"Failed to fetch release info: HTTP {}",
129+
response.status().as_u16()
130+
);
131+
}
132+
133+
let json: serde_json::Value = response
134+
.json()
135+
.context("Failed to parse release info as JSON")?;
136+
137+
let tag = json["tag_name"]
138+
.as_str()
139+
.context("Release missing tag_name")?;
140+
141+
// Strip 'v' prefix if present
142+
let version = tag.strip_prefix('v').unwrap_or(tag);
143+
Ok(version.to_string())
144+
}
145+
146+
/// Download a binary for a specific platform and version
147+
#[cfg(feature = "network")]
148+
fn download_binary(platform: Platform, version: &str, dest: &std::path::Path) -> Result<()> {
149+
let url = format!(
150+
"https://github.com/{}/releases/download/v{}/{}",
151+
GITHUB_REPO,
152+
version,
153+
platform.binary_name()
154+
);
155+
156+
let client = reqwest::blocking::Client::builder()
157+
.user_agent("rnr-cli")
158+
.build()
159+
.context("Failed to create HTTP client")?;
160+
161+
let response = client
162+
.get(&url)
163+
.send()
164+
.with_context(|| format!("Failed to download {}", platform.binary_name()))?;
165+
166+
if !response.status().is_success() {
167+
anyhow::bail!(
168+
"Failed to download {}: HTTP {}",
169+
platform.binary_name(),
170+
response.status().as_u16()
171+
);
172+
}
173+
174+
let bytes = response
175+
.bytes()
176+
.with_context(|| format!("Failed to read response for {}", platform.binary_name()))?;
177+
178+
// Write to file
179+
fs::write(dest, &bytes).with_context(|| format!("Failed to write {}", dest.display()))?;
180+
181+
// Make executable on Unix
182+
#[cfg(unix)]
183+
{
184+
use std::os::unix::fs::PermissionsExt;
185+
let mut perms = fs::metadata(dest)?.permissions();
186+
perms.set_mode(0o755);
187+
fs::set_permissions(dest, perms)?;
188+
}
189+
190+
Ok(())
191+
}
192+
193+
/// Compare semantic versions, returns true if latest is newer than current
194+
#[cfg(feature = "network")]
195+
fn is_newer_version(current: &str, latest: &str) -> bool {
196+
let parse_version = |v: &str| -> (u32, u32, u32) {
197+
let parts: Vec<&str> = v.split('.').collect();
198+
let major = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
199+
let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
200+
let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
201+
(major, minor, patch)
202+
};
203+
204+
let (cur_major, cur_minor, cur_patch) = parse_version(current);
205+
let (lat_major, lat_minor, lat_patch) = parse_version(latest);
206+
207+
if lat_major > cur_major {
208+
return true;
209+
}
210+
if lat_major == cur_major && lat_minor > cur_minor {
211+
return true;
212+
}
213+
if lat_major == cur_major && lat_minor == cur_minor && lat_patch > cur_patch {
214+
return true;
215+
}
216+
false
217+
}
218+
219+
#[cfg(test)]
220+
mod tests {
221+
use super::*;
222+
223+
#[test]
224+
#[cfg(feature = "network")]
225+
fn test_version_comparison() {
226+
assert!(is_newer_version("0.1.0", "0.2.0"));
227+
assert!(is_newer_version("0.1.0", "1.0.0"));
228+
assert!(is_newer_version("0.1.0", "0.1.1"));
229+
assert!(!is_newer_version("0.2.0", "0.1.0"));
230+
assert!(!is_newer_version("1.0.0", "0.9.0"));
231+
assert!(!is_newer_version("0.1.0", "0.1.0"));
232+
}
233+
}

0 commit comments

Comments
 (0)