From deda3dacba80bdb44516388bfec7c830ed1c82ab Mon Sep 17 00:00:00 2001 From: Alexander Ng Date: Sun, 27 Sep 2026 01:47:02 -0700 Subject: [PATCH 1/2] feat: show who envx auth authenticated as Co-Authored-By: Claude Opus 5.5 --- src/commands/auth/status.rs | 7 ++++++- tests/human_output.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/auth/status.rs b/src/commands/auth/status.rs index 23f9c6c..26e4903 100644 --- a/src/commands/auth/status.rs +++ b/src/commands/auth/status.rs @@ -23,6 +23,11 @@ pub async fn command(args: Args, config: &mut Config) -> anyhow::Result<()> { bail!("Key does not have a UUID, try `envx upload`"); } + let identity = format!( + "{} ({})", + crate::utils::messaging::safe(&key.primary_user_id), + key.fingerprint.chars().take(8).collect::() + ); let key = key.unlock(&password); let client = reqwest::Client::new(); @@ -47,7 +52,7 @@ pub async fn command(args: Args, config: &mut Config) -> anyhow::Result<()> { let status = res.status(); if status.is_success() { - println!("Authenticated successfully."); + println!("Authenticated as {identity}"); if args.debug || args.verbose { println!("{}", crate::utils::messaging::safe(&res.text().await?)); } diff --git a/tests/human_output.py b/tests/human_output.py index bdafc9e..a74aca7 100644 --- a/tests/human_output.py +++ b/tests/human_output.py @@ -6,6 +6,7 @@ import json import os from pathlib import Path +import re import shlex import sqlite3 import subprocess @@ -94,7 +95,7 @@ def do_DELETE(self): assert "From Teammate (@account)" in run("inbox") assert "Teammate (@account) · 22222222 · trusted" in run("friends") assert "Fingerprint: pin" in run("friends", "--verbose") -assert run("auth").strip() == "Authenticated successfully." +assert re.fullmatch(r"Authenticated as .+ \([0-9a-f]{8}\)", run("auth").strip()) assert "server response" in run("auth", "--verbose") for flags in [[], ["--verbose"], ["--json"]]: output = run("project", "remove-user", "--project-id", PROJECT, "--user-id", PEER, "--yes", *flags) From a2374ec9d016f58d328611ac470904ca22e9e377 Mon Sep 17 00:00:00 2001 From: Alexander Ng Date: Sun, 27 Sep 2026 01:48:15 -0700 Subject: [PATCH 2/2] feat: warn when Windows envx lives outside the standard folder `envx update` on Windows still updates in place but warns when the running binary is not in %LOCALAPPDATA%\Programs\envx, linking to the migration guide. install.ps1 warns when another envx earlier on PATH would shadow the one it installed. Co-Authored-By: Claude Opus 5.5 --- install.ps1 | 6 +++++ src/commands/update.rs | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/install.ps1 b/install.ps1 index fdc6bc1..2ed96a8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -122,4 +122,10 @@ Write-Host "Installed envx $version to $exe" Update-UserPath $true + + # An older manual install earlier on PATH would keep running instead. + $found = Get-Command envx -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($onWindows -and $found -and $found.Source -ine $exe) { + Write-Warning "Another envx at $($found.Source) comes first on PATH and will run instead. See https://envx.sh/docs/windows#migrate" + } } diff --git a/src/commands/update.rs b/src/commands/update.rs index 92919ad..c423fc1 100644 --- a/src/commands/update.rs +++ b/src/commands/update.rs @@ -126,6 +126,23 @@ async fn run_installer(script: &str) -> Result<()> { async fn run_installer(script: &str) -> Result<()> { let exe = std::env::current_exe()?; let dir = exe.parent().context("Executable has no parent folder")?; + let standard = std::env::var("LOCALAPPDATA").ok().map(|local| { + std::path::Path::new(&local).join("Programs").join("envx") + }); + if let Some(standard) = standard { + if !same_windows_dir( + &dir.to_string_lossy(), + &standard.to_string_lossy(), + ) { + eprintln!( + "{} envx is installed in {}, not the standard {}.\nUpdating it in place. To move it, see {}", + "Warning:".yellow().bold(), + dir.display(), + standard.display(), + WINDOWS_MIGRATE_URL, + ); + } + } let path = std::env::temp_dir() .join(format!("envx-install-{}.ps1", uuid::Uuid::new_v4())); std::fs::write(&path, script)?; @@ -153,6 +170,39 @@ async fn run_installer(script: &str) -> Result<()> { Ok(()) } +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +const WINDOWS_MIGRATE_URL: &str = "https://envx.sh/docs/windows#migrate"; + +/// Windows paths compare case-insensitively and ignore trailing separators. +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +fn same_windows_dir(a: &str, b: &str) -> bool { + let normalize = |p: &str| { + p.trim_start_matches(r"\\?\") + .replace('/', r"\") + .trim_end_matches('\\') + .to_lowercase() + }; + normalize(a) == normalize(b) +} + +#[cfg(test)] +mod windows_path_tests { + use super::same_windows_dir; + #[test] + fn standard_location_matches_regardless_of_case_and_separators() { + let standard = r"C:\Users\a\AppData\Local\Programs\envx"; + assert!(same_windows_dir( + r"c:\users\A\appdata\local\programs\envx\", + standard + )); + assert!(same_windows_dir( + r"\\?\C:\Users\a\AppData\Local\Programs\envx", + standard + )); + assert!(!same_windows_dir(r"C:\envx", standard)); + } +} + #[cfg(all(test, not(target_os = "windows")))] mod tests { use super::*;