From 4f2a7c6bebd76de434960720ba720d50c5e3ad0a Mon Sep 17 00:00:00 2001 From: Alireza Bashiri Date: Thu, 23 Jul 2026 19:15:29 +0700 Subject: [PATCH] fix(desktop): give Windows agent children a usable PATH and start dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things stop Buzz working on Windows. Managed agents get a PATH assembled from Buzz's own directories plus the user's login-shell PATH. On Windows that login-shell PATH is always None, because Git Bash reports POSIX colon-delimited entries that would be meaningless to a native child. The comment there says the child inherits the real Windows PATH instead, but it does not: the spawn calls `Command::env("PATH", ..)`, which replaces the inherited value rather than extending it. The child ends up with Buzz's managed directories and nothing else, so node, npm and git all disappear and every npm `.cmd` shim dies immediately: '"node"' is not recognized as an internal or external command ERROR buzz_acp: agent initialize failed: Agent process exited unexpectedly Error: all 24 agents failed to start — cannot continue Append the current process PATH on Windows when no shell PATH was supplied, gated on local context so callers passing nothing still get None back instead of a PATH manufactured out of ambient process state. Separately, `beforeDevCommand` runs `exec ./node_modules/.bin/vite`. `exec` is a POSIX shell builtin and Tauri runs the script through cmd.exe on Windows, so `tauri dev` cannot start at all: 'exec' is not recognized as an internal or external command The "beforeDevCommand" terminated with a non-zero status code. Invoke Vite through node instead, which works on every platform. This does drop the `exec` optimisation on Unix, where it saved an intermediate shell process, so it is worth a look from someone who runs the macOS dev loop daily. Signed-off-by: Alireza Bashiri --- .../src/managed_agents/runtime/path.rs | 56 +++++++++++++++++++ desktop/src-tauri/tauri.conf.json | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/desktop/src-tauri/src/managed_agents/runtime/path.rs b/desktop/src-tauri/src/managed_agents/runtime/path.rs index c1baadda7f..291f5bead9 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/path.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/path.rs @@ -11,6 +11,8 @@ use std::path::PathBuf; /// 4. `nvm_bin` — nvm's default Node.js bin dir (if the user uses nvm) /// 5. exe parent dir — DMG sidecars under `Contents/MacOS/` /// 6. user's login-shell `PATH` — runtimes like node/python from other managers +/// 7. Windows only: the current process `PATH`, since step 6 is always empty +/// there and callers replace rather than extend the child's `PATH` /// /// `shell_path` is the raw colon-delimited string from a login shell, so it is /// split into individual entries before joining. Pushing it as a single segment @@ -46,9 +48,30 @@ pub(in crate::managed_agents) fn build_augmented_path( if let Some(parent) = exe_parent { parts.push(parent); } + #[cfg_attr(not(windows), allow(unused_variables))] + let had_shell_path = shell_path.is_some(); if let Some(shell_path) = shell_path { parts.extend(std::env::split_paths(&shell_path)); } + + // Windows never supplies a login-shell PATH: `login_shell_path()` returns + // None there because Git Bash reports POSIX colon-delimited entries that + // would poison a native child. Nothing above contributes the user's real + // PATH, and the child does not fall back to its inherited one either — + // callers pass this value to `Command::env("PATH", ..)`, which replaces + // rather than extends. Without the process PATH the child loses node, npm + // and git, and every npm `.cmd` shim dies with + // `'"node"' is not recognized as an internal or external command`. + // Gated on local context for the same reason the managed dirs above are: + // callers that pass nothing must still get `None` back rather than a PATH + // manufactured out of ambient process state. + #[cfg(windows)] + if !had_shell_path && !parts.is_empty() { + parts.extend(std::env::split_paths( + &std::env::var_os("PATH").unwrap_or_default(), + )); + } + if parts.is_empty() { return None; } @@ -134,4 +157,37 @@ mod tests { assert!(result.starts_with("/home/user/.local/bin:"), "{result}"); assert!(result.ends_with(":/usr/local/bin"), "{result}"); } + + #[cfg(windows)] + #[test] + fn appends_process_path_when_no_shell_path() { + // Regression: `login_shell_path()` is always None on Windows, and + // callers overwrite the child's PATH rather than extending it, so + // without this the child loses node/npm/git and every npm `.cmd` shim + // fails with `'"node"' is not recognized`. + let _guard = crate::managed_agents::lock_path_mutex(); + let previous = std::env::var_os("PATH"); + std::env::set_var("PATH", r"C:\Program Files\nodejs"); + + let result = build_augmented_path(Some(PathBuf::from(r"C:\Users\agent")), None, None, None); + + match previous { + Some(value) => std::env::set_var("PATH", value), + None => std::env::remove_var("PATH"), + } + + let result = result.expect("path"); + assert!( + result.starts_with(r"C:\Users\agent\.local\bin;"), + "{result}" + ); + assert!(result.ends_with(r";C:\Program Files\nodejs"), "{result}"); + } + + #[cfg(windows)] + #[test] + fn process_path_not_manufactured_without_local_context() { + let _guard = crate::managed_agents::lock_path_mutex(); + assert_eq!(build_augmented_path(None, None, None, None), None); + } } diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index b75e0f5203..102b67b930 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -5,7 +5,7 @@ "identifier": "xyz.block.buzz.app", "build": { "beforeDevCommand": { - "script": "exec ./node_modules/.bin/vite", + "script": "node ./node_modules/vite/bin/vite.js", "cwd": "..", "wait": false },