Skip to content

Commit 2a07ca7

Browse files
committed
Check for updates in binaries
1 parent d32a379 commit 2a07ca7

File tree

2 files changed

+142
-43
lines changed

2 files changed

+142
-43
lines changed

src/cmd/run.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@ pub async fn run() -> Result<()> {
3434

3535
info!("Starting Cica with channels: {}", channels.join(", "));
3636

37-
// Ensure runtime dependencies are ready
3837
info!("Preparing runtime...");
39-
if let Err(e) = setup::ensure_embedding_model() {
40-
warn!("Failed to prepare embedding model: {}", e);
38+
if let Err(e) = setup::ensure_deps(&config).await {
39+
warn!("Failed to prepare dependencies: {}", e);
4140
}
4241

4342
// Index memories for all approved users at startup

src/setup.rs

Lines changed: 140 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,61 @@
22
33
use anyhow::{Context, Result, anyhow, bail};
44
use std::path::{Path, PathBuf};
5+
use tracing::info;
56

67
use crate::config;
78
use crate::memory;
89

9-
fn bun_download_url() -> Result<&'static str> {
10+
// ============================================================================
11+
// Pinned Versions
12+
// ============================================================================
13+
14+
const BUN_VERSION: &str = "1.2.4";
15+
const CLAUDE_CODE_VERSION: &str = "2.1.32";
16+
17+
const VERSION_FILE: &str = ".version";
18+
19+
/// Read the installed version from a dependency directory
20+
fn read_installed_version(dep_dir: &Path) -> Option<String> {
21+
std::fs::read_to_string(dep_dir.join(VERSION_FILE))
22+
.ok()
23+
.map(|s| s.trim().to_string())
24+
.filter(|s| !s.is_empty())
25+
}
26+
27+
/// Write the installed version marker to a dependency directory
28+
fn write_installed_version(dep_dir: &Path, version: &str) -> Result<()> {
29+
std::fs::write(dep_dir.join(VERSION_FILE), version)?;
30+
Ok(())
31+
}
32+
33+
/// Check if the installed version matches the expected version
34+
fn needs_update(dep_dir: &Path, expected: &str) -> bool {
35+
read_installed_version(dep_dir).as_deref() != Some(expected)
36+
}
37+
38+
// ============================================================================
39+
// Bun
40+
// ============================================================================
41+
42+
fn bun_download_url() -> Result<String> {
1043
match (std::env::consts::OS, std::env::consts::ARCH) {
11-
("macos", "aarch64") => {
12-
Ok("https://github.com/oven-sh/bun/releases/download/bun-v1.2.4/bun-darwin-aarch64.zip")
13-
}
14-
("macos", "x86_64") => {
15-
Ok("https://github.com/oven-sh/bun/releases/download/bun-v1.2.4/bun-darwin-x64.zip")
16-
}
17-
("linux", "aarch64") => {
18-
Ok("https://github.com/oven-sh/bun/releases/download/bun-v1.2.4/bun-linux-aarch64.zip")
19-
}
20-
("linux", "x86_64") => {
21-
Ok("https://github.com/oven-sh/bun/releases/download/bun-v1.2.4/bun-linux-x64.zip")
22-
}
44+
("macos", "aarch64") => Ok(format!(
45+
"https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-darwin-aarch64.zip",
46+
BUN_VERSION
47+
)),
48+
("macos", "x86_64") => Ok(format!(
49+
"https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-darwin-x64.zip",
50+
BUN_VERSION
51+
)),
52+
("linux", "aarch64") => Ok(format!(
53+
"https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-linux-aarch64.zip",
54+
BUN_VERSION
55+
)),
56+
("linux", "x86_64") => Ok(format!(
57+
"https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-linux-x64.zip",
58+
BUN_VERSION
59+
)),
2360
(os, arch) => bail!("Unsupported platform: {}-{}", os, arch),
2461
}
2562
}
@@ -42,29 +79,33 @@ pub fn find_bun() -> Option<PathBuf> {
4279
None
4380
}
4481

45-
/// Ensure Bun is available, downloading if necessary (async version)
82+
/// Ensure Bun is available and at the expected version
4683
pub async fn ensure_bun() -> Result<PathBuf> {
47-
// Check if already available
48-
if let Some(path) = find_bun() {
49-
return Ok(path);
84+
let paths = config::paths()?;
85+
86+
if find_bun().is_some() && !needs_update(&paths.bun_dir, BUN_VERSION) {
87+
return find_bun().ok_or_else(|| anyhow!("Bun not found"));
88+
}
89+
90+
if needs_update(&paths.bun_dir, BUN_VERSION) {
91+
info!("Updating Bun to v{}...", BUN_VERSION);
92+
let _ = std::fs::remove_dir_all(&paths.bun_dir);
5093
}
5194

52-
// Need to download
53-
let paths = config::paths()?;
5495
std::fs::create_dir_all(&paths.bun_dir)?;
5596

5697
let url = bun_download_url()?;
5798
let bun_path = paths.bun_dir.join("bun");
5899

59-
download_and_extract_bun(url, &paths.bun_dir).await?;
100+
download_and_extract_bun(&url, &paths.bun_dir).await?;
60101

61-
// Make executable
62102
#[cfg(unix)]
63103
{
64104
use std::os::unix::fs::PermissionsExt;
65105
std::fs::set_permissions(&bun_path, std::fs::Permissions::from_mode(0o755))?;
66106
}
67107

108+
write_installed_version(&paths.bun_dir, BUN_VERSION)?;
68109
Ok(bun_path)
69110
}
70111

@@ -115,20 +156,28 @@ pub fn find_claude_code() -> Option<PathBuf> {
115156
None
116157
}
117158

118-
/// Ensure Claude Code is available, downloading if necessary
159+
/// Ensure Claude Code is available and at the expected version
119160
pub async fn ensure_claude_code() -> Result<PathBuf> {
120-
if let Some(path) = find_claude_code() {
121-
return Ok(path);
161+
if find_claude_code().is_some()
162+
&& !needs_update(&config::paths()?.claude_code_dir, CLAUDE_CODE_VERSION)
163+
{
164+
return find_claude_code().ok_or_else(|| anyhow!("Claude Code not found"));
122165
}
123166

124167
let paths = config::paths()?;
168+
169+
if needs_update(&paths.claude_code_dir, CLAUDE_CODE_VERSION) {
170+
info!("Updating Claude Code to v{}...", CLAUDE_CODE_VERSION);
171+
let _ = std::fs::remove_dir_all(&paths.claude_code_dir);
172+
}
173+
125174
std::fs::create_dir_all(&paths.claude_code_dir)?;
126175

127-
// Use bun to install claude-code
128176
let bun = find_bun().ok_or_else(|| anyhow!("Bun not found - run ensure_bun first"))?;
177+
let pkg = format!("@anthropic-ai/claude-code@{}", CLAUDE_CODE_VERSION);
129178

130179
let status = tokio::process::Command::new(&bun)
131-
.args(["add", "@anthropic-ai/claude-code"])
180+
.args(["add", &pkg])
132181
.current_dir(&paths.claude_code_dir)
133182
.status()
134183
.await
@@ -138,6 +187,7 @@ pub async fn ensure_claude_code() -> Result<PathBuf> {
138187
bail!("Failed to install Claude Code");
139188
}
140189

190+
write_installed_version(&paths.claude_code_dir, CLAUDE_CODE_VERSION)?;
141191
find_claude_code().ok_or_else(|| anyhow!("Claude Code installation failed"))
142192
}
143193

@@ -217,9 +267,10 @@ fn validate_oauth_token(token: &str) -> Result<()> {
217267
}
218268

219269
// ============================================================================
220-
// Java (for signal-cli)
270+
// Java & signal-cli
221271
// ============================================================================
222272

273+
const JAVA_VERSION: &str = "21";
223274
const SIGNAL_CLI_VERSION: &str = "0.13.22";
224275

225276
fn java_download_url() -> Result<&'static str> {
@@ -270,18 +321,25 @@ pub fn find_java() -> Option<PathBuf> {
270321
None
271322
}
272323

273-
/// Ensure Java is available, downloading if necessary
324+
/// Ensure Java is available and at the expected version
274325
pub async fn ensure_java() -> Result<PathBuf> {
275-
if let Some(path) = find_java() {
276-
return Ok(path);
326+
let paths = config::paths()?;
327+
328+
if find_java().is_some() && !needs_update(&paths.java_dir, JAVA_VERSION) {
329+
return find_java().ok_or_else(|| anyhow!("Java not found"));
330+
}
331+
332+
if needs_update(&paths.java_dir, JAVA_VERSION) {
333+
info!("Updating Java JRE {}...", JAVA_VERSION);
334+
let _ = std::fs::remove_dir_all(&paths.java_dir);
277335
}
278336

279-
let paths = config::paths()?;
280337
std::fs::create_dir_all(&paths.java_dir)?;
281338

282339
let url = java_download_url()?;
283340
download_and_extract_tarball(url, &paths.java_dir).await?;
284341

342+
write_installed_version(&paths.java_dir, JAVA_VERSION)?;
285343
find_java()
286344
.ok_or_else(|| anyhow!("Java installation failed - binary not found after extraction"))
287345
}
@@ -309,18 +367,25 @@ pub fn find_signal_cli() -> Option<PathBuf> {
309367
None
310368
}
311369

312-
/// Ensure signal-cli is available, downloading if necessary
370+
/// Ensure signal-cli is available and at the expected version
313371
pub async fn ensure_signal_cli() -> Result<PathBuf> {
314-
if let Some(path) = find_signal_cli() {
315-
return Ok(path);
372+
let paths = config::paths()?;
373+
374+
if find_signal_cli().is_some() && !needs_update(&paths.signal_cli_dir, SIGNAL_CLI_VERSION) {
375+
return find_signal_cli().ok_or_else(|| anyhow!("signal-cli not found"));
376+
}
377+
378+
if needs_update(&paths.signal_cli_dir, SIGNAL_CLI_VERSION) {
379+
info!("Updating signal-cli to v{}...", SIGNAL_CLI_VERSION);
380+
let _ = std::fs::remove_dir_all(&paths.signal_cli_dir);
316381
}
317382

318-
let paths = config::paths()?;
319383
std::fs::create_dir_all(&paths.signal_cli_dir)?;
320384

321385
let url = signal_cli_download_url();
322386
download_and_extract_tarball(&url, &paths.signal_cli_dir).await?;
323387

388+
write_installed_version(&paths.signal_cli_dir, SIGNAL_CLI_VERSION)?;
324389
find_signal_cli().ok_or_else(|| {
325390
anyhow!("signal-cli installation failed - binary not found after extraction")
326391
})
@@ -379,19 +444,25 @@ pub fn find_cursor_cli() -> Option<PathBuf> {
379444
None
380445
}
381446

382-
/// Ensure Cursor CLI is available, downloading if necessary
447+
/// Ensure Cursor CLI is available and at the expected version
383448
pub async fn ensure_cursor_cli() -> Result<PathBuf> {
384-
if let Some(path) = find_cursor_cli() {
385-
return Ok(path);
449+
let paths = config::paths()?;
450+
451+
if find_cursor_cli().is_some() && !needs_update(&paths.cursor_cli_dir, CURSOR_CLI_VERSION) {
452+
return find_cursor_cli().ok_or_else(|| anyhow!("Cursor CLI not found"));
453+
}
454+
455+
if needs_update(&paths.cursor_cli_dir, CURSOR_CLI_VERSION) {
456+
info!("Updating Cursor CLI to {}...", CURSOR_CLI_VERSION);
457+
let _ = std::fs::remove_dir_all(&paths.cursor_cli_dir);
386458
}
387459

388-
let paths = config::paths()?;
389460
std::fs::create_dir_all(&paths.cursor_cli_dir)?;
390461
std::fs::create_dir_all(&paths.cursor_home)?;
391462

392-
// Download Cursor CLI
393463
download_cursor_cli(&paths.cursor_cli_dir).await?;
394464

465+
write_installed_version(&paths.cursor_cli_dir, CURSOR_CLI_VERSION)?;
395466
find_cursor_cli().ok_or_else(|| anyhow!("Cursor CLI installation failed"))
396467
}
397468

@@ -570,3 +641,32 @@ pub async fn validate_cursor_api_key(api_key: &str) -> Result<()> {
570641
pub fn ensure_embedding_model() -> Result<()> {
571642
memory::ensure_model_downloaded()
572643
}
644+
645+
// ============================================================================
646+
// Startup Dependency Check
647+
// ============================================================================
648+
649+
/// Ensure all dependencies for the active backend are installed and up to date.
650+
/// Called on `cica run` startup.
651+
pub async fn ensure_deps(config: &crate::config::Config) -> Result<()> {
652+
use crate::config::AiBackend;
653+
654+
match config.backend {
655+
AiBackend::Claude => {
656+
ensure_bun().await?;
657+
ensure_claude_code().await?;
658+
}
659+
AiBackend::Cursor => {
660+
ensure_bun().await?;
661+
ensure_cursor_cli().await?;
662+
}
663+
}
664+
665+
if config.channels.signal.is_some() {
666+
ensure_java().await?;
667+
ensure_signal_cli().await?;
668+
}
669+
670+
ensure_embedding_model()?;
671+
Ok(())
672+
}

0 commit comments

Comments
 (0)