fix(nest): use buzz-dev symlink name for dev builds (#1587)

Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Will Pfleger
2026-07-07 12:17:53 -04:00
committed by GitHub
co-authored by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent 49391d8071
commit dcbb3ff789
3 changed files with 85 additions and 14 deletions
+4 -1
View File
@@ -82,7 +82,10 @@ const overrides = new Map([
// to split with the rest of this list.
// dev-nest namespace: OnceLock<Option<PathBuf>> + init_nest_dir + constants
// added to plumb the dev/prod discriminator. Load-bearing for the D2 nest fix.
["src-tauri/src/managed_agents/nest.rs", 1501],
// dev-build CLI symlink: cli_link_name helper + is_dev param on
// ensure_cli_symlink + prod/dev test variants add ~68 lines. Load-bearing;
// queued to split with the rest of this list.
["src-tauri/src/managed_agents/nest.rs", 1569],
// harness-persona-sync: persona-runtime resolution threaded into the spawn
// path here. Load-bearing feature growth; queued to split in the resolver
// unify refactor followup. +26 for resolve_effective_prompt_model_provider
+1 -1
View File
@@ -323,7 +323,7 @@ pub fn run() {
// bundled CLI binary. Non-fatal: agents find CLI via PATH.
if let Ok(exe) = std::env::current_exe() {
if let Some(parent) = exe.parent() {
if let Err(error) = managed_agents::ensure_cli_symlink(parent) {
if let Err(error) = managed_agents::ensure_cli_symlink(parent, is_dev_nest) {
eprintln!("buzz-desktop: failed to create CLI symlink: {error}");
}
}
+80 -12
View File
@@ -306,16 +306,35 @@ fn ensure_skill_symlinks(_root: &Path) -> Result<(), String> {
Ok(())
}
/// Ensures `~/.local/bin/buzz` is a symlink to the bundled CLI binary.
/// Returns the `~/.local/bin` link name for the bundled CLI.
///
/// On every boot: replaces any existing symlink unconditionally (the `buzz`
/// name is our namespace), creates a new one if absent, and leaves regular
/// files alone to avoid clobbering a user-compiled binary.
/// Dev builds (`is_dev = true`) use `"buzz-dev"` so that a running DMG and a
/// concurrent dev build each own a separate link and never clobber each other —
/// the same isolation that separates `~/.buzz` (prod) from `~/.buzz-dev` (dev).
pub fn cli_link_name(is_dev: bool) -> &'static str {
if is_dev {
"buzz-dev"
} else {
"buzz"
}
}
/// Ensures `~/.local/bin/buzz` (prod) or `~/.local/bin/buzz-dev` (dev) is a
/// symlink to the bundled CLI binary.
///
/// The link name is split by `is_dev` so that an installed DMG and a
/// concurrently running dev build each maintain their own symlink and never
/// overwrite each other's target — the same isolation that separates the
/// `~/.buzz` and `~/.buzz-dev` nests (see [`NEST_DIR_DEV`]).
///
/// On every boot: replaces any existing symlink unconditionally (the `buzz` /
/// `buzz-dev` name is our namespace), creates a new one if absent, and leaves
/// regular files alone to avoid clobbering a user-compiled binary.
///
/// Non-fatal: callers should ignore errors — the symlink is a convenience
/// for human Terminal use; agents find the CLI via PATH augmentation.
#[cfg(unix)]
pub fn ensure_cli_symlink(exe_parent: &Path) -> Result<(), String> {
pub fn ensure_cli_symlink(exe_parent: &Path, is_dev: bool) -> Result<(), String> {
let buzz_bin = exe_parent.join("buzz");
if !buzz_bin.exists() {
return Ok(()); // CLI not bundled (e.g., dev builds without sidecars).
@@ -327,7 +346,7 @@ pub fn ensure_cli_symlink(exe_parent: &Path) -> Result<(), String> {
.join("bin");
fs::create_dir_all(&local_bin).map_err(|e| format!("create {}: {e}", local_bin.display()))?;
let link = local_bin.join("buzz");
let link = local_bin.join(cli_link_name(is_dev));
match link.symlink_metadata() {
Ok(meta) if meta.file_type().is_symlink() => {
let _ = fs::remove_file(&link);
@@ -351,7 +370,7 @@ pub fn ensure_cli_symlink(exe_parent: &Path) -> Result<(), String> {
/// No-op on non-Unix platforms — symlink management is macOS/Linux only.
#[cfg(not(unix))]
pub fn ensure_cli_symlink(_exe_parent: &Path) -> Result<(), String> {
pub fn ensure_cli_symlink(_exe_parent: &Path, _is_dev: bool) -> Result<(), String> {
Ok(())
}
@@ -983,19 +1002,29 @@ mod tests {
);
}
#[test]
fn cli_link_name_prod_is_buzz() {
assert_eq!(cli_link_name(false), "buzz");
}
#[test]
fn cli_link_name_dev_is_buzz_dev() {
assert_eq!(cli_link_name(true), "buzz-dev");
}
#[cfg(unix)]
#[test]
fn ensure_cli_symlink_creates_symlink() {
fn ensure_cli_symlink_creates_symlink_prod() {
let tmp = tempfile::tempdir().unwrap();
let exe_parent = tmp.path().join("MacOS");
fs::create_dir(&exe_parent).unwrap();
fs::write(exe_parent.join("buzz"), "binary").unwrap();
// Simulate the symlink creation path.
let local_bin = tmp.path().join("local_bin");
fs::create_dir_all(&local_bin).unwrap();
let link = local_bin.join("buzz");
// Prod link name is "buzz"; simulate the symlink creation path.
let link = local_bin.join(cli_link_name(false));
std::os::unix::fs::symlink(exe_parent.join("buzz"), &link).unwrap();
assert!(link.symlink_metadata().unwrap().file_type().is_symlink());
assert_eq!(fs::read_link(&link).unwrap(), exe_parent.join("buzz"));
@@ -1003,11 +1032,33 @@ mod tests {
#[cfg(unix)]
#[test]
fn ensure_cli_symlink_does_not_clobber_regular_file() {
fn ensure_cli_symlink_creates_symlink_dev() {
let tmp = tempfile::tempdir().unwrap();
let exe_parent = tmp.path().join("MacOS");
fs::create_dir(&exe_parent).unwrap();
fs::write(exe_parent.join("buzz"), "binary").unwrap();
let local_bin = tmp.path().join("local_bin");
fs::create_dir_all(&local_bin).unwrap();
// Dev link must be "buzz-dev", never "buzz".
assert_eq!(cli_link_name(true), "buzz-dev");
let link = local_bin.join(cli_link_name(true));
std::os::unix::fs::symlink(exe_parent.join("buzz"), &link).unwrap();
assert!(link.symlink_metadata().unwrap().file_type().is_symlink());
assert_eq!(fs::read_link(&link).unwrap(), exe_parent.join("buzz"));
// Prod link must not exist — the two builds don't touch each other.
assert!(!local_bin.join("buzz").exists());
}
#[cfg(unix)]
#[test]
fn ensure_cli_symlink_does_not_clobber_regular_file_prod() {
let tmp = tempfile::tempdir().unwrap();
let local_bin = tmp.path().join("local_bin");
fs::create_dir_all(&local_bin).unwrap();
let link = local_bin.join("buzz");
let link = local_bin.join(cli_link_name(false));
fs::write(&link, "user-installed binary").unwrap();
// Regular files are preserved — the Ok(_) branch skips them.
@@ -1015,6 +1066,23 @@ mod tests {
assert_eq!(fs::read_to_string(&link).unwrap(), "user-installed binary");
}
#[cfg(unix)]
#[test]
fn ensure_cli_symlink_does_not_clobber_regular_file_dev() {
let tmp = tempfile::tempdir().unwrap();
let local_bin = tmp.path().join("local_bin");
fs::create_dir_all(&local_bin).unwrap();
let link = local_bin.join(cli_link_name(true));
fs::write(&link, "user-installed buzz-dev binary").unwrap();
// Regular files at the dev path are also preserved.
assert!(link.symlink_metadata().unwrap().file_type().is_file());
assert_eq!(
fs::read_to_string(&link).unwrap(),
"user-installed buzz-dev binary"
);
}
fn make_persona(id: &str, display_name: &str) -> PersonaRecord {
PersonaRecord {
id: id.to_string(),