Skip to content

Commit e7c5bf3

Browse files
committed
v0.9.9: burnwall upgrade (alias self-upgrade)
One command to move to the latest release: stops the proxy (a running burnwall.exe can't be overwritten on Windows), installs, restarts. On Windows renames its own running binary aside so the installer can write the new one (restores on failure). --dry-run / --no-restart. Mirror of self-rollback.
1 parent 1f06411 commit e7c5bf3

8 files changed

Lines changed: 186 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
All notable changes to Burnwall.
44

5+
## [0.9.9] — 2026-06-08
6+
7+
### Added
8+
9+
- **`burnwall upgrade`** (alias `self-upgrade`) — one command to move to the
10+
latest release. It stops the running proxy first (a live `burnwall.exe` can't
11+
be overwritten on Windows), runs the installer, and restarts the proxy. On
12+
Windows it renames its own running binary aside so the installer can write the
13+
new one, restoring it if the install fails. `--dry-run` to preview,
14+
`--no-restart` to skip the restart. The mirror of `self-rollback`.
15+
516
## [0.9.8] — 2026-06-07
617

718
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "burnwall"
3-
version = "0.9.8"
3+
version = "0.9.9"
44
edition = "2024"
55
rust-version = "1.87"
66
description = "Local proxy for AI coding tools (Claude Code, Codex CLI, Aider): cache-aware cost tracking, path/command security checks, daily budget enforcement. Zero telemetry."

editor/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "burnwall",
33
"displayName": "Burnwall",
44
"description": "Cost + security for your AI coding agents, at a glance — reads your local Burnwall CLI.",
5-
"version": "0.9.8",
5+
"version": "0.9.9",
66
"publisher": "intbot",
77
"license": "FSL-1.1-MIT",
88
"repository": { "type": "git", "url": "https://github.com/intbot/burnwall" },

packaging/mcp/server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"url": "https://github.com/intbot/burnwall",
77
"source": "github"
88
},
9-
"version": "0.9.8",
9+
"version": "0.9.9",
1010
"packages": [
1111
{
1212
"registryType": "oci",

src/cli/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub mod service;
3838
pub mod start;
3939
pub mod status;
4040
pub mod statusline;
41+
pub mod upgrade;
4142
pub mod stop;
4243
pub mod watch;
4344
#[cfg(feature = "waste")]
@@ -107,6 +108,9 @@ pub enum Command {
107108
UninstallService(service::UninstallServiceArgs),
108109
/// Roll back to a prior burnwall release via the dist installer.
109110
SelfRollback(self_rollback::SelfRollbackArgs),
111+
/// Upgrade to the latest release (stops the proxy, installs, restarts).
112+
#[command(visible_alias = "self-upgrade")]
113+
Upgrade(upgrade::UpgradeArgs),
110114
/// Inspect and manage the pricing rate card (local + signed remote cards).
111115
Pricing(pricing::PricingArgs),
112116
/// Render the Burnwall ribbon for Claude Code's status line (reads stdin JSON).
@@ -157,6 +161,7 @@ impl Cli {
157161
Command::InstallService(args) => service::install_cmd(args),
158162
Command::UninstallService(args) => service::uninstall_cmd(args),
159163
Command::SelfRollback(args) => self_rollback::run_cmd(args),
164+
Command::Upgrade(args) => upgrade::run_cmd(args),
160165
Command::Pricing(args) => pricing::run_cmd(args),
161166
Command::Statusline(args) => statusline::run_cmd(args),
162167
Command::Watch(args) => watch::run_cmd(args),

src/cli/upgrade.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//! `burnwall upgrade` (alias `self-upgrade`) — fetch and install the latest
2+
//! release, handling the two things that make a manual `irm … | iex` fail:
3+
//!
4+
//! 1. **A running proxy holds `burnwall.exe` open** — Windows can't overwrite a
5+
//! live executable. We stop the proxy first (and restart it after).
6+
//! 2. **The upgrade process IS `burnwall.exe`** — it holds its *own* file. On
7+
//! Windows we rename our running binary aside (`burnwall.exe.old`, which is
8+
//! permitted even while running) so the installer can write a fresh one; the
9+
//! stale `.old` is cleaned up on the next upgrade.
10+
//!
11+
//! Mirror of [`super::self_rollback`], which goes the other direction.
12+
13+
#[cfg(windows)]
14+
use std::path::Path;
15+
16+
use anyhow::{Context, Result};
17+
use clap::Args;
18+
19+
const REPO: &str = "intbot/burnwall";
20+
21+
#[derive(Args, Debug)]
22+
pub struct UpgradeArgs {
23+
/// Print what would run without doing it.
24+
#[arg(long)]
25+
pub dry_run: bool,
26+
/// Don't restart the proxy afterward, even if it was running.
27+
#[arg(long)]
28+
pub no_restart: bool,
29+
}
30+
31+
pub fn run_cmd(args: UpgradeArgs) -> Result<()> {
32+
let url = installer_url();
33+
println!("⬆ Upgrading Burnwall to the latest release");
34+
println!(" Installer URL: {url}");
35+
36+
if args.dry_run {
37+
println!(" Would: stop the proxy (if running) → run the installer → restart it.");
38+
if cfg!(windows) {
39+
println!(" Would run: irm {url} | iex");
40+
} else {
41+
println!(" Would run: curl --proto '=https' --tlsv1.2 -LsSf {url} | sh");
42+
}
43+
return Ok(());
44+
}
45+
46+
// 1. Stop the running proxy so the binary can be replaced.
47+
let was_running = matches!(super::daemon::running_pid(), Ok(Some(_)));
48+
if was_running {
49+
println!(" Stopping the running proxy so the binary can be replaced…");
50+
let _ = super::stop::run_cmd(super::stop::StopArgs {});
51+
}
52+
53+
// The canonical install path, captured before any rename so the restart
54+
// targets the freshly-written binary.
55+
let exe = std::env::current_exe().context("locating the burnwall executable")?;
56+
57+
// 2. Install the latest release.
58+
#[cfg(windows)]
59+
win_upgrade(&url, &exe)?;
60+
#[cfg(not(windows))]
61+
run_installer(&url)?;
62+
63+
println!(" ✓ Installed the latest release.");
64+
65+
// 3. Restart the proxy if it was running.
66+
if was_running && !args.no_restart {
67+
match std::process::Command::new(&exe)
68+
.args(["start", "--daemon"])
69+
.status()
70+
{
71+
Ok(s) if s.success() => println!(" Restarted the proxy on the new version."),
72+
_ => println!(" (could not auto-restart — run `burnwall start --daemon`)"),
73+
}
74+
} else if was_running {
75+
println!(" (not restarted — run `burnwall start --daemon` when ready)");
76+
}
77+
Ok(())
78+
}
79+
80+
fn installer_url() -> String {
81+
// `releases/latest/download/…` always resolves to the newest release asset.
82+
let filename = if cfg!(windows) {
83+
"burnwall-installer.ps1"
84+
} else {
85+
"burnwall-installer.sh"
86+
};
87+
format!("https://github.com/{REPO}/releases/latest/download/{filename}")
88+
}
89+
90+
#[cfg(not(windows))]
91+
fn run_installer(url: &str) -> Result<()> {
92+
let status = std::process::Command::new("sh")
93+
.arg("-c")
94+
.arg(format!("curl --proto '=https' --tlsv1.2 -LsSf '{url}' | sh"))
95+
.status()
96+
.context("running shell installer")?;
97+
if !status.success() {
98+
anyhow::bail!("installer exited with status {status}");
99+
}
100+
Ok(())
101+
}
102+
103+
/// Windows: rename our own running binary aside so the installer can write a
104+
/// fresh one at the original path, then restore on failure.
105+
#[cfg(windows)]
106+
fn win_upgrade(url: &str, exe: &Path) -> Result<()> {
107+
let old = exe.with_extension("exe.old");
108+
// Best-effort: clear a leftover from a previous upgrade.
109+
let _ = std::fs::remove_file(&old);
110+
// Windows permits renaming a running executable (it can't overwrite it).
111+
std::fs::rename(exe, &old)
112+
.with_context(|| format!("moving current binary aside ({} → .old)", exe.display()))?;
113+
114+
let result = run_installer_ps(url);
115+
if result.is_err() {
116+
// Restore the original binary so we never leave the user without one.
117+
let _ = std::fs::rename(&old, exe);
118+
}
119+
result
120+
}
121+
122+
#[cfg(windows)]
123+
fn run_installer_ps(url: &str) -> Result<()> {
124+
let status = std::process::Command::new("powershell.exe")
125+
.args([
126+
"-NoProfile",
127+
"-ExecutionPolicy",
128+
"Bypass",
129+
"-Command",
130+
&format!("irm {url} | iex"),
131+
])
132+
.status()
133+
.context("running PowerShell installer")?;
134+
if !status.success() {
135+
anyhow::bail!("installer exited with status {status}");
136+
}
137+
Ok(())
138+
}

tests/integration/cli_test.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,3 +818,31 @@ fn share_no_sign_emits_unsigned_card() {
818818
.success()
819819
.stdout(predicate::str::contains("unsigned"));
820820
}
821+
822+
// ─────────────────────────────── upgrade ───────────────────────────────
823+
824+
#[test]
825+
fn upgrade_dry_run_prints_plan() {
826+
let dir = tempfile::tempdir().unwrap();
827+
let path = dir.path().to_path_buf();
828+
fs::create_dir_all(&path).unwrap();
829+
burnwall(&path)
830+
.args(["upgrade", "--dry-run"])
831+
.assert()
832+
.success()
833+
.stdout(predicate::str::contains("latest release"))
834+
.stdout(predicate::str::contains("releases/latest/download"))
835+
.stdout(predicate::str::contains("stop the proxy"));
836+
}
837+
838+
#[test]
839+
fn self_upgrade_alias_works() {
840+
let dir = tempfile::tempdir().unwrap();
841+
let path = dir.path().to_path_buf();
842+
fs::create_dir_all(&path).unwrap();
843+
burnwall(&path)
844+
.args(["self-upgrade", "--dry-run"])
845+
.assert()
846+
.success()
847+
.stdout(predicate::str::contains("Upgrading Burnwall"));
848+
}

0 commit comments

Comments
 (0)