diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38eee93..bdcd514 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,17 +120,14 @@ jobs: echo "$VPS_SSH_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H 46.225.185.232 >> ~/.ssh/known_hosts - scp -i ~/.ssh/deploy_key \ - target/x86_64-unknown-linux-gnu/release/helios-remote-relay \ - root@46.225.185.232:/opt/helios-remote/target/release/helios-remote-relay-new - # Also publish relay binary to download URL for self-update + # Only publish to download URL — relay updates itself when triggered by CLI scp -i ~/.ssh/deploy_key \ target/x86_64-unknown-linux-gnu/release/helios-remote-relay \ root@46.225.185.232:/var/www/helios-remote/helios-remote-relay-linux - ssh -i ~/.ssh/deploy_key root@46.225.185.232 \ - "mv /opt/helios-remote/target/release/helios-remote-relay-new \ - /opt/helios-remote/target/release/helios-remote-relay && \ - systemctl restart helios-remote" + # Write version.json so CLI knows what's available + echo "{\"commit\":\"$(git rev-parse --short HEAD)\"}" > version.json + scp -i ~/.ssh/deploy_key version.json \ + root@46.225.185.232:/var/www/helios-remote/version.json build-cli: runs-on: ubuntu-latest @@ -142,10 +139,10 @@ jobs: - name: Install Rust (stable) + targets uses: dtolnay/rust-toolchain@stable with: - targets: x86_64-unknown-linux-gnu,x86_64-pc-windows-gnu + targets: x86_64-unknown-linux-gnu,x86_64-pc-windows-gnu,aarch64-unknown-linux-gnu - name: Install cross-compilers - run: sudo apt-get update && sudo apt-get install -y gcc-x86-64-linux-gnu gcc-mingw-w64-x86-64 mingw-w64-tools + run: sudo apt-get update && sudo apt-get install -y gcc-x86-64-linux-gnu gcc-mingw-w64-x86-64 mingw-w64-tools gcc-aarch64-linux-gnu - name: Cache dependencies uses: Swatinem/rust-cache@v2 @@ -157,6 +154,11 @@ jobs: env: CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: x86_64-linux-gnu-gcc + - name: Build CLI (Linux aarch64) + run: cargo build --release --package helios-remote-cli --target aarch64-unknown-linux-gnu + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + - name: Build CLI (Windows x86_64) run: cargo build --release --package helios-remote-cli --target x86_64-pc-windows-gnu env: @@ -188,6 +190,9 @@ jobs: scp -i ~/.ssh/deploy_key \ target/x86_64-unknown-linux-gnu/release/helios-remote-cli \ root@46.225.185.232:/var/www/helios-remote/helios-remote-cli-linux + scp -i ~/.ssh/deploy_key \ + target/aarch64-unknown-linux-gnu/release/helios-remote-cli \ + root@46.225.185.232:/var/www/helios-remote/helios-remote-cli-linux-aarch64 scp -i ~/.ssh/deploy_key \ target/x86_64-pc-windows-gnu/release/helios-remote-cli.exe \ root@46.225.185.232:/var/www/helios-remote/helios-remote-cli-windows.exe diff --git a/README.md b/README.md index fce9145..c0f4c22 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ irm https://raw.githubusercontent.com/agent-helios/helios-remote/master/scripts/ ``` AI Agent │ - ▼ remote CLI -helios-server ──WebSocket── helios-client (Windows) + ▼ helios-remote-cli +helios-remote-relay ──WebSocket── helios-remote-client (Windows) ``` 1. The **Windows client** connects to the relay server via WebSocket and registers with its device label. @@ -62,11 +62,22 @@ remote clipboard-get # get clipboard text remote clipboard-set # set clipboard text remote upload # upload file to device remote download # download file from device -remote version # compare relay/helios/client commits +remote version # compare latest/relay/cli/client commits +remote update # update all components to latest version remote logs # fetch last 20 lines of client log (default) remote logs --lines 200 # custom line count ``` +### Update System + +`remote update ` checks `version.json` on the download server for the latest available commit and updates any component that's behind: + +- **Relay** — downloads new binary, replaces itself, restarts via systemd +- **Client** — downloads new binary, replaces itself, relaunches automatically +- **CLI** — downloads new binary, replaces itself, re-executes the update command + +CI publishes new binaries after every push to `master` but does **not** auto-restart the relay. Updates only happen when explicitly triggered via `remote update`. + --- ## Server Setup @@ -102,3 +113,4 @@ The relay server (`helios-remote-relay`) runs on the VPS and is not distributed. MIT + diff --git a/SKILL.md b/SKILL.md index ddef201..a98d4c3 100644 --- a/SKILL.md +++ b/SKILL.md @@ -73,12 +73,16 @@ $SKILL_DIR/helios clipboard-set moritz-pc "Text for clipboard" $SKILL_DIR/helios upload moritz-pc /tmp/local.txt "C:\Users\Moritz\Desktop\remote.txt" $SKILL_DIR/helios download moritz-pc "C:\Users\Moritz\file.txt" /tmp/downloaded.txt -# Version: compare relay + remote + client commits (are they in sync?) -$SKILL_DIR/helios version moritz-pc +# Version: compare latest available vs running commits (relay / cli / client) +$SKILL_DIR/remote version moritz-pc -# Client log (last 100 lines, --lines for more) -$SKILL_DIR/helios logs moritz-pc -$SKILL_DIR/helios logs moritz-pc --lines 200 +# Update: bring all components (relay, cli, client) to latest version +# CI publishes new binaries but does NOT auto-restart — this triggers the actual update +$SKILL_DIR/remote update moritz-pc + +# Client log (last 20 lines by default, --lines for more) +$SKILL_DIR/remote logs moritz-pc +$SKILL_DIR/remote logs moritz-pc --lines 200 ``` ## Typical Workflow: UI Task diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 8cf2bef..94de807 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -9,7 +9,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4", features = ["derive"] } -reqwest = { version = "0.12", features = ["blocking", "json"] } +reqwest = { version = "0.12", features = ["blocking", "json", "rustls-tls"], default-features = false } base64 = "0.22" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 51f6361..17ad146 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -722,7 +722,17 @@ fn main() { Commands::Update { device } => { validate_label(&device); - // Fetch all three commits + // Fetch latest available commit from version.json + let latest_commit = match reqwest::blocking::get("https://agent-helios.me/downloads/helios-remote/version.json") { + Ok(r) => r + .json::() + .ok() + .and_then(|v| v["commit"].as_str().map(String::from)) + .unwrap_or_else(|| "?".into()), + Err(e) => format!("error: {}", e), + }; + + // Fetch all three running commits let relay_commit = match reqwest::blocking::get(&format!("{}/version", cfg.base_url)) { Ok(r) => r .json::() @@ -739,13 +749,14 @@ fn main() { let cli_commit = GIT_COMMIT; + println!(" latest {}", latest_commit); println!(" relay {}", relay_commit); println!(" cli {}", cli_commit); println!(" client {}", client_commit); - let all_same = relay_commit == cli_commit && cli_commit == client_commit; - if all_same { - println!(" ✅ Already up to date (commit: {})", cli_commit); + let all_current = relay_commit == latest_commit && cli_commit == latest_commit && client_commit == latest_commit; + if all_current { + println!(" ✅ Already up to date (commit: {})", latest_commit); return; } @@ -753,7 +764,7 @@ fn main() { let mut updated_any = false; // Update relay if needed - if relay_commit != cli_commit { + if relay_commit != latest_commit { println!(" → Updating relay..."); let data = req(&cfg, "POST", "/relay/update", None, 15); println!(" {}", data["message"].as_str().unwrap_or("triggered")); @@ -761,7 +772,7 @@ fn main() { } // Update client if needed - if client_commit != cli_commit { + if client_commit != latest_commit { println!(" → Updating client on {}...", device); let data = req( &cfg, @@ -780,18 +791,13 @@ fn main() { } // Self-update CLI if needed - // (relay_commit is the "canonical" latest — if we differ from it, we're outdated) - // Skip on non-x86_64 Linux (e.g. ARM/Pi) — CI only builds x86_64 Linux binaries - #[cfg(all(not(target_os = "windows"), not(target_arch = "x86_64")))] - if cli_commit != relay_commit { - println!(" → Skipping CLI update (non-x86_64, update manually)"); - } - #[cfg(any(target_os = "windows", target_arch = "x86_64"))] - if cli_commit != relay_commit { + if cli_commit != latest_commit { println!(" → Updating CLI..."); #[cfg(target_os = "windows")] let url = "https://agent-helios.me/downloads/helios-remote/helios-remote-cli-windows.exe"; - #[cfg(not(target_os = "windows"))] + #[cfg(all(not(target_os = "windows"), target_arch = "aarch64"))] + let url = "https://agent-helios.me/downloads/helios-remote/helios-remote-cli-linux-aarch64"; + #[cfg(all(not(target_os = "windows"), not(target_arch = "aarch64")))] let url = "https://agent-helios.me/downloads/helios-remote/helios-remote-cli-linux"; let bytes = match reqwest::blocking::get(url) { diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 1a07545..fdd6afa 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -221,6 +221,13 @@ async fn main() { banner(); + // Clean up leftover .old.exe from previous self-update (Windows can't delete running exe) + #[cfg(target_os = "windows")] + if let Ok(exe) = std::env::current_exe() { + let old = exe.with_extension("old.exe"); + let _ = std::fs::remove_file(&old); + } + // Single instance check if !acquire_instance_lock() { display::err("❌", "Another instance of helios-remote is already running."); @@ -712,13 +719,17 @@ async fn handle_message( display::cmd_done("🔄", "update", "", true, "updated — restarting"); // Delete old binary let _ = std::fs::remove_file(&old); + // Release single-instance lock so new process can start + release_instance_lock(); // Restart with same args (new console window on Windows) let args: Vec = std::env::args().skip(1).collect(); #[cfg(target_os = "windows")] { - use std::os::windows::process::CommandExt; - const CREATE_NEW_CONSOLE: u32 = 0x00000010; - let _ = std::process::Command::new(&exe).args(&args).creation_flags(CREATE_NEW_CONSOLE).spawn(); + // Use "start" to open a new visible console window + let exe_str = exe.to_string_lossy(); + let _ = std::process::Command::new("cmd") + .args(["/c", "start", "", &exe_str]) + .spawn(); } #[cfg(not(target_os = "windows"))] let _ = std::process::Command::new(&exe).args(&args).spawn();