aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 21:53:59 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 21:54:29 +0300
commit6f459691056b68c2c99d8341b5f8dd4b811a53c1 (patch)
tree24c780f3813945feef4c0b48be1645ac22067691
parent0a635a3fc1e971030079945a447c73e2b24489f8 (diff)
fix(update): keep windows installs on preview
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--docs/next/README.md8
-rw-r--r--docs/next/website/src/content/docs/cli-reference.mdx2
-rw-r--r--docs/next/website/src/content/docs/configuration.mdx2
-rw-r--r--docs/next/website/src/content/docs/index.mdx2
-rw-r--r--docs/next/website/src/content/docs/install.mdx10
-rw-r--r--docs/next/website/src/content/docs/windows-beta.mdx6
-rw-r--r--src/cli.rs46
-rw-r--r--src/config/model.rs15
-rw-r--r--src/update.rs94
-rw-r--r--website/index.html2
-rw-r--r--website/install.ps167
12 files changed, 203 insertions, 52 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index 259b5bc5..1214b4d4 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -3,6 +3,7 @@
## Unreleased
### Fixed
+- Windows beta installs now default to the preview channel, persist `channel = "preview"` during install, reject switching to stable until stable Windows builds exist, and update through the Windows installer without probing unsupported live-handoff/session status paths.
- Pane output now renders flag emoji and other multi-codepoint grapheme clusters as complete symbols instead of blank cells. (#243)
- Starting Herdr with no restored workspaces, or closing the last workspace, now opens a default workspace instead of leaving the client on an empty screen where direct keybindings such as `cmd+n` were shown but ignored. (#366)
- Resizing restored panes no longer aborts the server when libghostty-vt reflows a terminal whose pre-resize cursor row is past the new height. (#465)
diff --git a/docs/next/README.md b/docs/next/README.md
index 6d1d6919..d6cc7ded 100644
--- a/docs/next/README.md
+++ b/docs/next/README.md
@@ -28,7 +28,7 @@ curl -fsSL https://herdr.dev/install.sh | sh
on windows preview beta:
```powershell
-$env:HERDR_CHANNEL="preview"; powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"
+powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"
```
or install with homebrew:
@@ -79,15 +79,15 @@ Herdr notifies you when a new version is available. Run manually:
herdr update
```
-`herdr update` is for installs managed by Herdr's own installer. Homebrew, mise, and Nix installs update through `brew upgrade herdr`, `mise upgrade herdr`, or your Nix workflow, then use the same stop-and-run-again flow if a session is still running the old server. Direct installs can opt into development preview builds with `herdr channel set preview` and return to stable with `herdr channel set stable`. See [install docs](https://herdr.dev/docs/install/) and [session state docs](https://herdr.dev/docs/session-state/) for the full update, restart, restore, and handoff matrix.
+`herdr update` is for installs managed by Herdr's own installer. Homebrew, mise, and Nix installs update through `brew upgrade herdr`, `mise upgrade herdr`, or your Nix workflow, then use the same stop-and-run-again flow if a session is still running the old server. Linux and macOS direct installs can opt into development preview builds with `herdr channel set preview` and return to stable with `herdr channel set stable`. Windows beta installs are preview-only for now. See [install docs](https://herdr.dev/docs/install/) and [session state docs](https://herdr.dev/docs/session-state/) for the full update, restart, restore, and handoff matrix.
-Herdr uses the stable update channel by default. To test preview builds from `master` before the next stable release:
+Linux and macOS direct installs use the stable update channel by default. Windows beta installs default to preview. To test preview builds from `master` before the next stable release:
```bash
herdr channel set preview
```
-To return to stable:
+To return Linux and macOS direct installs to stable:
```bash
herdr channel set stable
diff --git a/docs/next/website/src/content/docs/cli-reference.mdx b/docs/next/website/src/content/docs/cli-reference.mdx
index 61ca1f28..ebcdc2b4 100644
--- a/docs/next/website/src/content/docs/cli-reference.mdx
+++ b/docs/next/website/src/content/docs/cli-reference.mdx
@@ -21,7 +21,7 @@ herdr update # download and install from the configured channel
herdr update --handoff # opt into live handoff for supported running servers
herdr channel show # print stable or preview
herdr channel set preview # opt into preview builds
-herdr channel set stable # return to stable releases
+herdr channel set stable # return Linux/macOS direct installs to stable
herdr --version # print version
```
diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx
index 97a6a36a..ebe28aa2 100644
--- a/docs/next/website/src/content/docs/configuration.mdx
+++ b/docs/next/website/src/content/docs/configuration.mdx
@@ -35,7 +35,7 @@ onboarding = false
## Update channel
-Direct installs use the stable update channel by default.
+Linux and macOS direct installs use the stable update channel by default. Windows beta installs default to preview and cannot switch to stable until stable Windows releases are available.
```toml
[update]
diff --git a/docs/next/website/src/content/docs/index.mdx b/docs/next/website/src/content/docs/index.mdx
index bc54dcab..eadb3920 100644
--- a/docs/next/website/src/content/docs/index.mdx
+++ b/docs/next/website/src/content/docs/index.mdx
@@ -26,7 +26,7 @@ herdr
On Windows preview beta:
```powershell
-$env:HERDR_CHANNEL="preview"; powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"
+powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"
herdr
```
diff --git a/docs/next/website/src/content/docs/install.mdx b/docs/next/website/src/content/docs/install.mdx
index 1fa355da..fd9b87c9 100644
--- a/docs/next/website/src/content/docs/install.mdx
+++ b/docs/next/website/src/content/docs/install.mdx
@@ -16,10 +16,10 @@ curl -fsSL https://herdr.dev/install.sh | sh
On Windows preview beta, install the preview channel:
```powershell
-$env:HERDR_CHANNEL="preview"; powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"
+powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"
```
-The installer downloads the right release binary for your platform and places it on your PATH. The Windows installer uses versioned install folders and updates a `current` junction, so updates do not need to overwrite a running `herdr.exe`.
+The installer downloads the right release binary for your platform and places it on your PATH. The Windows installer defaults to preview, writes that channel to Herdr's config, uses versioned install folders, and updates a `current` junction, so updates do not need to overwrite a running `herdr.exe`.
## Install with Homebrew
@@ -57,13 +57,13 @@ herdr update
`herdr update` is for installs managed by Herdr's own installer. Homebrew, mise, and Nix installs are updated through those package managers instead.
-Herdr uses the stable update channel by default. To opt into preview builds from `master`, set the channel:
+On Linux and macOS, Herdr uses the stable update channel by default. To opt into preview builds from `master`, set the channel:
```bash
herdr channel set preview
```
-Switch back to stable the same way:
+Switch Linux and macOS direct installs back to stable the same way:
```bash
herdr channel set stable
@@ -73,6 +73,8 @@ For direct installs, changing channels also checks that channel and installs its
Preview builds are GitHub prereleases published from the current development branch. They are useful when you want fixes before the next stable release, but they can regress. Homebrew, mise, and Nix installs do not use the preview channel.
+Windows beta builds are preview-only for now. `herdr channel set stable` is rejected on Windows until stable Windows releases are available.
+
By default, `herdr update` installs the new binary and leaves compatible running sessions alone. If an update changes Herdr's client/server protocol, Herdr asks whether to stop the old server after installing. Stop the old server to use the new version. Stopping exits pane processes. For the default session, run `herdr server stop`, then run `herdr` again. For a named session, run `herdr session stop <name>`, then run `herdr session attach <name>` again.
To opt into experimental live server handoff for supported running sessions, run:
diff --git a/docs/next/website/src/content/docs/windows-beta.mdx b/docs/next/website/src/content/docs/windows-beta.mdx
index 489feb61..762da9ef 100644
--- a/docs/next/website/src/content/docs/windows-beta.mdx
+++ b/docs/next/website/src/content/docs/windows-beta.mdx
@@ -12,10 +12,10 @@ The goal of the beta is to learn from real use: install success, pane reliabilit
Install native Windows beta builds with PowerShell:
```powershell
-$env:HERDR_CHANNEL="preview"; powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"
+powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"
```
-Windows beta builds ship only through the preview channel. The Windows installer stores releases under `%USERPROFILE%\.herdr\packages\standalone\releases`, points `%LOCALAPPDATA%\Programs\Herdr\bin` at the current release, and keeps a small number of older releases so running processes do not block updates.
+Windows beta builds ship only through the preview channel. The Windows installer defaults to preview, writes `channel = "preview"` to Herdr's config, stores releases under `%USERPROFILE%\.herdr\packages\standalone\releases`, points `%LOCALAPPDATA%\Programs\Herdr\bin` at the current release, and keeps a small number of older releases so running processes do not block updates.
For internal beta testing, `HERDR_MANIFEST_URL` can point the installer at a custom manifest instead of Herdr's stable or preview manifest.
@@ -82,7 +82,7 @@ herdr
That mode runs Herdr on the remote host. Native Windows `herdr --remote` is not part of the beta.
-Windows updates may require stopping the running Herdr server and starting it again. Live handoff is Unix-only.
+Windows updates run through the Windows installer and update the versioned install junction. Restart running Herdr sessions after updating. Live handoff is Unix-only.
## Reporting Windows beta issues
diff --git a/src/cli.rs b/src/cli.rs
index 3650ebdb..88408fc8 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -79,7 +79,7 @@ fn channel_set(args: &[String]) -> std::io::Result<i32> {
return Ok(2);
};
- if let Some(reason) = channel_set_preview_rejection(
+ if let Some(reason) = channel_set_rejection(
channel,
crate::update::preview_channel_rejection_for_current_install(),
) {
@@ -151,13 +151,21 @@ fn parse_channel_set_arg(args: &[String]) -> Option<&str> {
}
}
-fn channel_set_preview_rejection(
+fn channel_set_rejection(
channel: &str,
install_rejection: Option<&'static str>,
) -> Option<&'static str> {
- (channel == "preview")
- .then_some(install_rejection)
- .flatten()
+ if cfg!(windows) && channel == "stable" {
+ return Some(
+ "stable channel is not available on Windows yet; Windows builds are preview-only",
+ );
+ }
+
+ if channel == "preview" {
+ return install_rejection;
+ }
+
+ None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -898,14 +906,34 @@ mod tests {
#[test]
fn channel_set_rejects_package_managed_preview_before_config_write() {
assert_eq!(
- super::channel_set_preview_rejection("preview", Some("no preview")),
+ super::channel_set_rejection("preview", Some("no preview")),
Some("no preview")
);
assert_eq!(
- super::channel_set_preview_rejection("stable", Some("no preview")),
- None
+ super::channel_set_rejection("stable", Some("no preview")),
+ if cfg!(windows) {
+ Some(
+ "stable channel is not available on Windows yet; Windows builds are preview-only",
+ )
+ } else {
+ None
+ }
+ );
+ assert_eq!(super::channel_set_rejection("preview", None), None);
+ }
+
+ #[test]
+ fn channel_set_rejects_stable_only_on_windows() {
+ assert_eq!(
+ super::channel_set_rejection("stable", None),
+ if cfg!(windows) {
+ Some(
+ "stable channel is not available on Windows yet; Windows builds are preview-only",
+ )
+ } else {
+ None
+ }
);
- assert_eq!(super::channel_set_preview_rejection("preview", None), None);
}
#[test]
diff --git a/src/config/model.rs b/src/config/model.rs
index c36150f6..528a940c 100644
--- a/src/config/model.rs
+++ b/src/config/model.rs
@@ -30,17 +30,26 @@ impl UpdateChannelConfig {
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(default)]
pub struct UpdateConfig {
+ #[serde(default = "default_update_channel")]
pub channel: UpdateChannelConfig,
}
impl Default for UpdateConfig {
fn default() -> Self {
Self {
- channel: UpdateChannelConfig::Stable,
+ channel: default_update_channel(),
}
}
}
+fn default_update_channel() -> UpdateChannelConfig {
+ if cfg!(windows) {
+ UpdateChannelConfig::Preview
+ } else {
+ UpdateChannelConfig::Stable
+ }
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ToastDelivery {
@@ -714,9 +723,9 @@ mod tests {
use super::*;
#[test]
- fn update_channel_defaults_stable_and_parses() {
+ fn update_channel_defaults_for_platform_and_parses() {
let default_config = Config::default();
- assert_eq!(default_config.update.channel, UpdateChannelConfig::Stable);
+ assert_eq!(default_config.update.channel, default_update_channel());
let toml = r#"
[update]
diff --git a/src/update.rs b/src/update.rs
index 3337d6e9..bedcc1b0 100644
--- a/src/update.rs
+++ b/src/update.rs
@@ -10,11 +10,16 @@ use std::collections::BTreeMap;
use std::env;
#[cfg(not(windows))]
use std::fs;
-use std::io::{self, BufRead, BufReader, IsTerminal, Write};
+#[cfg(not(windows))]
+use std::io;
+#[cfg(not(windows))]
+use std::io::{BufRead, BufReader, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
+#[cfg(not(windows))]
use std::time::{Duration, Instant};
+#[cfg(not(windows))]
use interprocess::local_socket::traits::Stream as _;
use serde::{Deserialize, Deserializer};
@@ -29,9 +34,13 @@ const MISE_INSTALLS_DIR_ENV: &str = "MISE_INSTALLS_DIR";
const FAKE_UPDATE_VERSION_ENV: &str = "HERDR_FAKE_UPDATE_VERSION";
const FAKE_UPDATE_NOTES_VERSION_ENV: &str = "HERDR_FAKE_UPDATE_NOTES_VERSION";
const DEFAULT_FAKE_UPDATE_NOTES_VERSION: &str = "0.3.0";
+#[cfg(not(windows))]
const SERVER_STOP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
+#[cfg(not(windows))]
const SERVER_HANDOFF_REQUEST_TIMEOUT: Duration = Duration::from_secs(240);
+#[cfg(not(windows))]
const SERVER_HANDOFF_CONFIRM_TIMEOUT: Duration = Duration::from_secs(30);
+#[cfg(not(windows))]
const SERVER_SHUTDOWN_POLL_INTERVAL: Duration = Duration::from_millis(100);
fn fake_release_notes_body(version: &str) -> String {
let notes_version = env::var(FAKE_UPDATE_NOTES_VERSION_ENV)
@@ -156,6 +165,7 @@ impl<'de> Deserialize<'de> for AssetRef {
struct UpdateManifest {
version: String,
/// Thin-client protocol spoken by this release, when advertised by the manifest.
+ #[cfg(not(windows))]
protocol: Option<u32>,
notes: String,
assets: BTreeMap<String, AssetRef>,
@@ -259,6 +269,7 @@ struct ReleaseInfo {
channel: UpdateChannel,
build_id: Option<String>,
commit: Option<String>,
+ #[cfg(not(windows))]
target_protocol: Option<u32>,
download_url: String,
sha256: Option<String>,
@@ -358,6 +369,7 @@ fn release_info_from_manifest(manifest: &UpdateManifest) -> Result<Option<Releas
channel: UpdateChannel::Stable,
build_id: None,
commit: None,
+ #[cfg(not(windows))]
target_protocol: manifest.protocol,
download_url,
sha256: asset.sha256.clone(),
@@ -442,6 +454,7 @@ fn release_info_from_preview_manifest(
channel: UpdateChannel::Preview,
build_id: Some(build_id.to_string()),
commit: Some(manifest.commit.clone()),
+ #[cfg(not(windows))]
target_protocol: Some(manifest.protocol),
download_url,
sha256: asset.sha256.clone(),
@@ -662,6 +675,7 @@ fn running_inside_herdr() -> bool {
running_inside_herdr_env(env::var(crate::HERDR_ENV_VAR).ok().as_deref())
}
+#[cfg(not(windows))]
fn client_protocol_server_is_running_at(socket_path: &Path) -> bool {
if !socket_path.exists() {
return false;
@@ -670,14 +684,17 @@ fn client_protocol_server_is_running_at(socket_path: &Path) -> bool {
crate::ipc::connect_local_stream(socket_path).is_ok()
}
+#[cfg(not(windows))]
fn client_protocol_server_is_running() -> bool {
client_protocol_server_is_running_at(&crate::server::socket_paths::client_socket_path())
}
+#[cfg(not(windows))]
fn version_label(version: Option<&str>) -> &str {
version.unwrap_or("unknown")
}
+#[cfg(not(windows))]
fn update_requires_server_restart(
server: &crate::api::RuntimeStatus,
release: &ReleaseInfo,
@@ -688,6 +705,7 @@ fn update_requires_server_restart(
}
}
+#[cfg(not(windows))]
fn server_supports_live_handoff(server: &crate::api::RuntimeStatus) -> bool {
server
.capabilities
@@ -695,6 +713,7 @@ fn server_supports_live_handoff(server: &crate::api::RuntimeStatus) -> bool {
.is_some_and(|capabilities| capabilities.live_handoff)
}
+#[cfg(not(windows))]
fn parse_stop_old_servers_after_update_response(input: &str, default_yes: bool) -> Option<bool> {
let trimmed = input.trim().to_ascii_lowercase();
match trimmed.as_str() {
@@ -706,12 +725,14 @@ fn parse_stop_old_servers_after_update_response(input: &str, default_yes: bool)
}
#[derive(Debug, Clone, PartialEq, Eq)]
+#[cfg(not(windows))]
struct RunningServerUpdatePlan {
target: RunningUpdateTarget,
server: crate::api::RuntimeStatus,
requires_server_restart: bool,
}
+#[cfg(not(windows))]
impl RunningServerUpdatePlan {
fn label(&self) -> &str {
&self.target.label
@@ -739,12 +760,14 @@ impl RunningServerUpdatePlan {
}
#[derive(Debug, Clone, PartialEq, Eq)]
+#[cfg(not(windows))]
struct RunningServerUpdateDecision {
plan: RunningServerUpdatePlan,
action: RunningServerUpdateAction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[cfg(not(windows))]
enum RunningServerUpdateAction {
None,
LiveHandoff,
@@ -752,6 +775,7 @@ enum RunningServerUpdateAction {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[cfg(not(windows))]
enum RunningServerUpdateOutcome {
RestartDeferred,
Stopped,
@@ -763,6 +787,7 @@ enum RunningServerUpdateOutcome {
}
#[derive(Debug, Clone, PartialEq, Eq)]
+#[cfg(not(windows))]
struct RunningSessionUpdateOutcome {
session_label: String,
target_noun: &'static str,
@@ -773,6 +798,7 @@ struct RunningSessionUpdateOutcome {
}
#[derive(Debug, Clone, PartialEq, Eq)]
+#[cfg(not(windows))]
enum FailedHandoffServerState {
UpdatedServerRunning,
OldServerRunning(crate::api::RuntimeStatus),
@@ -780,6 +806,7 @@ enum FailedHandoffServerState {
Unknown(String),
}
+#[cfg(not(windows))]
fn plan_running_server_updates(
release: &ReleaseInfo,
) -> Result<Vec<RunningServerUpdatePlan>, String> {
@@ -837,6 +864,7 @@ fn plan_running_server_updates(
}
#[derive(Debug, Clone, PartialEq, Eq)]
+#[cfg(not(windows))]
struct RunningUpdateTarget {
name: Option<String>,
label: String,
@@ -847,6 +875,7 @@ struct RunningUpdateTarget {
must_be_running: bool,
}
+#[cfg(not(windows))]
fn running_update_targets() -> Result<Vec<RunningUpdateTarget>, String> {
if crate::session::explicit_session_requested() {
return Ok(vec![RunningUpdateTarget {
@@ -913,6 +942,7 @@ fn running_update_targets() -> Result<Vec<RunningUpdateTarget>, String> {
.collect())
}
+#[cfg(not(windows))]
fn target_client_protocol_server_is_running() -> Result<bool, String> {
if crate::session::explicit_session_requested()
|| std::env::var_os(crate::api::SOCKET_PATH_ENV_VAR).is_some()
@@ -951,6 +981,7 @@ pub(crate) fn parse_self_update_args(args: &[String]) -> Result<SelfUpdateOption
Ok(options)
}
+#[cfg(not(windows))]
fn prompt_to_stop_old_servers_before_update(
plans: &[RunningServerUpdatePlan],
release: &ReleaseInfo,
@@ -999,6 +1030,7 @@ fn prompt_to_stop_old_servers_before_update(
}
}
+#[cfg(not(windows))]
fn confirm_running_server_update_action(
plans: Vec<RunningServerUpdatePlan>,
release: &ReleaseInfo,
@@ -1053,6 +1085,7 @@ fn confirm_running_server_update_action(
Ok(decisions)
}
+#[cfg(not(windows))]
fn target_group_nouns(plans: &[&RunningServerUpdatePlan]) -> (&'static str, &'static str) {
let all_sessions = plans.iter().all(|plan| plan.target_noun() == "session");
let all_servers = plans.iter().all(|plan| plan.target_noun() == "server");
@@ -1065,6 +1098,7 @@ fn target_group_nouns(plans: &[&RunningServerUpdatePlan]) -> (&'static str, &'st
}
}
+#[cfg(not(windows))]
fn prompt_to_complete_plain_update(
decisions: &[RunningServerUpdateDecision],
release: &ReleaseInfo,
@@ -1121,6 +1155,7 @@ fn prompt_to_complete_plain_update(
}
}
+#[cfg(not(windows))]
fn mark_plain_update_stop_decisions(
decisions: Vec<RunningServerUpdateDecision>,
) -> Vec<RunningServerUpdateDecision> {
@@ -1133,6 +1168,7 @@ fn mark_plain_update_stop_decisions(
.collect()
}
+#[cfg(not(windows))]
fn print_running_session_update_summary(
plans: &[RunningServerUpdatePlan],
release: &ReleaseInfo,
@@ -1164,6 +1200,7 @@ fn print_running_session_update_summary(
eprintln!();
}
+#[cfg(not(windows))]
fn live_handoff_running_server_for_update(
plan: &RunningServerUpdatePlan,
release: &ReleaseInfo,
@@ -1184,6 +1221,7 @@ fn live_handoff_running_server_for_update(
Ok(())
}
+#[cfg(not(windows))]
fn runtime_matches_release(status: &crate::api::RuntimeStatus, release: &ReleaseInfo) -> bool {
let protocol_matches = release
.target_protocol
@@ -1192,6 +1230,7 @@ fn runtime_matches_release(status: &crate::api::RuntimeStatus, release: &Release
protocol_matches && version_matches
}
+#[cfg(not(windows))]
fn classify_failed_live_handoff_state_at(
socket_path: &Path,
release: &ReleaseInfo,
@@ -1206,6 +1245,7 @@ fn classify_failed_live_handoff_state_at(
}
}
+#[cfg(not(windows))]
fn prompt_to_stop_old_server_after_failed_handoff(
plan: &RunningServerUpdatePlan,
release: &ReleaseInfo,
@@ -1254,6 +1294,7 @@ fn prompt_to_stop_old_server_after_failed_handoff(
}
}
+#[cfg(not(windows))]
fn recover_failed_live_handoff_for_update(
plan: &RunningServerUpdatePlan,
release: &ReleaseInfo,
@@ -1310,6 +1351,7 @@ fn recover_failed_live_handoff_for_update(
}
}
+#[cfg(not(windows))]
fn reconnect_or_stop_guidance(plan: &RunningServerUpdatePlan) -> String {
if let Some(command) = plan.attach_command() {
format!(
@@ -1324,6 +1366,7 @@ fn reconnect_or_stop_guidance(plan: &RunningServerUpdatePlan) -> String {
}
}
+#[cfg(not(windows))]
fn stop_server_via_api_at(socket_path: &Path, timeout: Duration) -> Result<(), String> {
use crate::api::schema::{EmptyParams, Method};
@@ -1336,6 +1379,7 @@ fn stop_server_via_api_at(socket_path: &Path, timeout: Duration) -> Result<(), S
)
}
+#[cfg(not(windows))]
fn send_server_update_method_at(
socket_path: &Path,
timeout: Duration,
@@ -1404,6 +1448,7 @@ fn live_handoff_server_via_api_at(socket_path: &Path, timeout: Duration) -> Resu
)
}
+#[cfg(not(windows))]
fn live_handoff_server_via_api_for_release_at(
socket_path: &Path,
timeout: Duration,
@@ -1427,6 +1472,7 @@ fn live_handoff_server_via_api_for_release_at(
)
}
+#[cfg(not(windows))]
fn live_handoff_server_via_api_for_update_at(
socket_path: &Path,
updated_exe: &Path,
@@ -1440,6 +1486,7 @@ fn live_handoff_server_via_api_for_update_at(
)
}
+#[cfg(not(windows))]
fn server_shutdown_confirmed_at(socket_path: &Path) -> Result<bool, String> {
if !socket_path.exists() {
return Ok(true);
@@ -1464,6 +1511,7 @@ fn server_shutdown_confirmed_at(socket_path: &Path) -> Result<bool, String> {
}
}
+#[cfg(not(windows))]
fn wait_for_server_shutdown_at(socket_path: &Path, timeout: Duration) -> Result<(), String> {
let deadline = Instant::now() + timeout;
loop {
@@ -1481,6 +1529,7 @@ fn wait_for_server_shutdown_at(socket_path: &Path, timeout: Duration) -> Result<
}
}
+#[cfg(not(windows))]
fn stop_running_server_for_update(plan: &RunningServerUpdatePlan) -> Result<(), String> {
eprintln!("stopping herdr {} {}...", plan.target_noun(), plan.label());
stop_server_via_api_at(plan.socket_path(), SERVER_STOP_RESPONSE_TIMEOUT)?;
@@ -1488,6 +1537,7 @@ fn stop_running_server_for_update(plan: &RunningServerUpdatePlan) -> Result<(),
Ok(())
}
+#[cfg(not(windows))]
fn wait_for_server_handoff_at(
socket_path: &Path,
timeout: Duration,
@@ -1501,6 +1551,7 @@ fn wait_for_server_handoff_at(
)
}
+#[cfg(not(windows))]
fn wait_for_running_server_protocol_at(
socket_path: &Path,
timeout: Duration,
@@ -1532,6 +1583,7 @@ fn wait_for_running_server_protocol_at(
}
}
+#[cfg(not(windows))]
fn apply_running_session_update_decisions(
release: &ReleaseInfo,
updated_exe: &Path,
@@ -1570,6 +1622,7 @@ fn apply_running_session_update_decisions(
Ok(outcomes)
}
+#[cfg(not(windows))]
fn print_running_session_update_outcomes(
outcomes: &[RunningSessionUpdateOutcome],
release: &ReleaseInfo,
@@ -1886,6 +1939,13 @@ fn homebrew_cellar_keg_root(path: &Path) -> Option<PathBuf> {
/// Manual self-update command (`herdr update`).
pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
let channel = UpdateChannel::configured();
+ #[cfg(windows)]
+ if channel == UpdateChannel::Stable {
+ return Err(
+ "Windows builds are preview-only for now; run `herdr channel set preview`".into(),
+ );
+ }
+
if is_homebrew_managed_install() {
if channel == UpdateChannel::Preview {
return Err(
@@ -1935,10 +1995,6 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
}
};
- let running_server_plans = plan_running_server_updates(&release)?;
- let server_update_decisions =
- confirm_running_server_update_action(running_server_plans, &release, options)?;
-
if let Some(commit) = &release.commit {
tracing::info!(commit = %commit, build_id = ?release.build_id, "selected preview update build");
}
@@ -1948,13 +2004,7 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
#[cfg(windows)]
{
- if !options.live_handoff
- && !prompt_to_complete_plain_update(&server_update_decisions, &release)?
- {
- eprintln!("Herdr was not updated.");
- eprintln!("Stop running Herdr sessions when ready, then run `herdr update` again.");
- return Ok(current);
- }
+ let _ = options;
eprintln!(
"installing {} with the Windows installer...",
@@ -1966,23 +2016,19 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
install_windows_update_with_installer(channel)?;
let updated_exe = windows_installed_herdr_exe_path()?;
eprintln!("installed {}", release.label());
- let server_update_decisions = if options.live_handoff {
- server_update_decisions
- } else {
- mark_plain_update_stop_decisions(server_update_decisions)
- };
- let server_update_outcomes = apply_running_session_update_decisions(
- &release,
- &updated_exe,
- server_update_decisions,
- )?;
print_outdated_integration_notice_with_updated_binary(&updated_exe);
-
- print_running_session_update_outcomes(&server_update_outcomes, &release);
+ eprintln!(
+ "Restart any running Herdr sessions to use {}.",
+ release.label()
+ );
}
#[cfg(not(windows))]
{
+ let running_server_plans = plan_running_server_updates(&release)?;
+ let server_update_decisions =
+ confirm_running_server_update_action(running_server_plans, &release, options)?;
+
eprintln!("downloading {}...", release.label());
let downloaded_update = download_update(&release)?;
let updated_exe = downloaded_update.current_exe.clone();
diff --git a/website/index.html b/website/index.html
index 1a06efc0..b27d3cd6 100644
--- a/website/index.html
+++ b/website/index.html
@@ -175,7 +175,7 @@
<div class="install-card install-card-secondary" aria-label="Windows beta install command">
<span class="prompt">PS&gt;</span>
<code id="windows-install-cmd" class="is-compact"
- >$env:HERDR_CHANNEL="preview"; irm https://herdr.dev/install.ps1 | iex</code
+ >irm https://herdr.dev/install.ps1 | iex</code
>
<button type="button" data-copy-target="windows-install-cmd">Copy</button>
</div>
diff --git a/website/install.ps1 b/website/install.ps1
index d07a5f1c..ca22974f 100644
--- a/website/install.ps1
+++ b/website/install.ps1
@@ -12,7 +12,7 @@ $ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
if ([string]::IsNullOrWhiteSpace($Channel)) {
- $Channel = "stable"
+ $Channel = "preview"
}
function Write-Step {
@@ -283,6 +283,64 @@ function Resolve-HerdrVersion {
return [string]$Manifest.version
}
+function Get-HerdrConfigPath {
+ if (-not [string]::IsNullOrWhiteSpace($env:HERDR_CONFIG_PATH)) {
+ return $env:HERDR_CONFIG_PATH
+ }
+
+ $appDir = "herdr"
+ if (-not [string]::IsNullOrWhiteSpace($env:XDG_CONFIG_HOME)) {
+ return Join-Path (Join-Path $env:XDG_CONFIG_HOME $appDir) "config.toml"
+ }
+ if (-not [string]::IsNullOrWhiteSpace($env:APPDATA)) {
+ return Join-Path (Join-Path $env:APPDATA $appDir) "config.toml"
+ }
+ if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) {
+ return Join-Path (Join-Path (Join-Path (Join-Path $env:USERPROFILE "AppData") "Roaming") $appDir) "config.toml"
+ }
+ if (-not [string]::IsNullOrWhiteSpace($env:HOME)) {
+ return Join-Path (Join-Path (Join-Path $env:HOME ".config") $appDir) "config.toml"
+ }
+
+ return Join-Path (Join-Path ([System.IO.Path]::GetTempPath()) $appDir) "config.toml"
+}
+
+function Set-HerdrUpdateChannelConfig {
+ param([string]$SelectedChannel)
+
+ $configPath = Get-HerdrConfigPath
+ $configDir = Split-Path -Parent $configPath
+ if (-not [string]::IsNullOrWhiteSpace($configDir)) {
+ New-Item -ItemType Directory -Force -Path $configDir | Out-Null
+ }
+
+ $content = if (Test-Path -LiteralPath $configPath -PathType Leaf) {
+ Get-Content -LiteralPath $configPath -Raw
+ } else {
+ ""
+ }
+
+ $valueLine = "channel = `"$SelectedChannel`""
+ $match = [regex]::Match($content, "(?ms)^(\s*\[update\]\s*)(.*?)(?=^\s*\[|\z)")
+ if ($match.Success) {
+ $body = $match.Groups[2].Value
+ if ($body -match "(?m)^\s*channel\s*=") {
+ $updatedBody = [regex]::Replace($body, "(?m)^\s*channel\s*=.*$", $valueLine, 1)
+ } else {
+ $updatedBody = $body.TrimEnd() + "`r`n$valueLine`r`n"
+ }
+ $bodyStart = $match.Groups[2].Index
+ $updated = $content.Substring(0, $bodyStart) + $updatedBody + $content.Substring($bodyStart + $body.Length)
+ } elseif ([string]::IsNullOrWhiteSpace($content)) {
+ $updated = "[update]`r`n$valueLine`r`n"
+ } else {
+ $updated = $content.TrimEnd() + "`r`n`r`n[update]`r`n$valueLine`r`n"
+ }
+
+ Set-Content -LiteralPath $configPath -Value $updated -NoNewline
+ Write-Step "Herdr update channel set to $SelectedChannel in $configPath."
+}
+
if ($env:OS -ne "Windows_NT") {
Write-Error "install.ps1 supports Windows only. Use install.sh on Linux or macOS."
exit 1
@@ -293,6 +351,11 @@ if (-not [Environment]::Is64BitOperatingSystem) {
exit 1
}
+if ($Channel -eq "stable") {
+ Write-Error "Windows builds are preview-only for now. Omit -Channel or use -Channel preview."
+ exit 1
+}
+
$architecture = [System.Runtime.InteropServices.RuntimeInformation,mscorlib]::OSArchitecture.ToString()
switch ($architecture) {
"X64" {
@@ -383,6 +446,8 @@ try {
Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
+Set-HerdrUpdateChannelConfig -SelectedChannel $Channel
+
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
$newUserPath = Prepend-PathEntry -PathValue $userPath -Entry $visibleBinDir
if ($newUserPath -cne $userPath) {