mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Bundle sprout CLI in DMG release + agent PATH discoverability (#614)
This commit is contained in:
@@ -349,6 +349,7 @@ jobs:
|
||||
touch "desktop/src-tauri/binaries/sprout-agent-$TARGET"
|
||||
touch "desktop/src-tauri/binaries/sprout-dev-mcp-$TARGET"
|
||||
touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET"
|
||||
touch "desktop/src-tauri/binaries/sprout-$TARGET"
|
||||
- name: Build Tauri app
|
||||
run: cd desktop && pnpm tauri build
|
||||
env:
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
- name: Build sidecars
|
||||
run: |
|
||||
cargo build --release -p sprout-acp -p sprout-mcp -p sprout-agent -p sprout-dev-mcp -p git-credential-nostr
|
||||
cargo build --release -p sprout-acp -p sprout-mcp -p sprout-agent -p sprout-dev-mcp -p git-credential-nostr -p sprout-cli
|
||||
./scripts/bundle-sidecars.sh
|
||||
|
||||
- name: Build unsigned Tauri app
|
||||
|
||||
Generated
+19
-19
@@ -5225,7 +5225,25 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sprout"
|
||||
name = "sprout-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
"nostr 0.36.0",
|
||||
"percent-encoding",
|
||||
"rand 0.10.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"subtle",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sprout-desktop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"atomic-write-file",
|
||||
@@ -5280,24 +5298,6 @@ dependencies = [
|
||||
"zip 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sprout-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
"nostr 0.36.0",
|
||||
"percent-encoding",
|
||||
"rand 0.10.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"subtle",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sprout-persona"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "sprout"
|
||||
name = "sprout-desktop"
|
||||
version = "0.1.0"
|
||||
description = "Sprout desktop app"
|
||||
authors = ["you"]
|
||||
|
||||
@@ -393,6 +393,16 @@ pub fn run() {
|
||||
eprintln!("sprout-desktop: failed to create nest: {error}");
|
||||
}
|
||||
|
||||
// Create/update ~/.local/bin/sprout symlink pointing to the
|
||||
// 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) {
|
||||
eprintln!("sprout-desktop: failed to create CLI symlink: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-download voice models in the background so they're ready
|
||||
// when the user starts their first huddle. Idempotent — no-op if
|
||||
// already downloaded. ~289 MB total (~100 MB Parakeet STT + ~189 MB Pocket TTS).
|
||||
|
||||
@@ -122,6 +122,64 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensures `~/.local/bin/sprout` is a symlink to the bundled CLI binary.
|
||||
///
|
||||
/// Creates the symlink if it doesn't exist, updates it if it already points
|
||||
/// to a Sprout app bundle, and leaves it alone if it points elsewhere (to
|
||||
/// avoid clobbering another tool's 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> {
|
||||
let sprout_bin = exe_parent.join("sprout");
|
||||
if !sprout_bin.exists() {
|
||||
return Ok(()); // CLI not bundled (e.g., dev builds without sidecars).
|
||||
}
|
||||
|
||||
let local_bin = dirs::home_dir()
|
||||
.ok_or("cannot resolve home directory")?
|
||||
.join(".local")
|
||||
.join("bin");
|
||||
fs::create_dir_all(&local_bin).map_err(|e| format!("create {}: {e}", local_bin.display()))?;
|
||||
|
||||
let link = local_bin.join("sprout");
|
||||
match link.symlink_metadata() {
|
||||
Ok(meta) if meta.file_type().is_symlink() => {
|
||||
// Symlink exists — only update if it points to a Sprout bundle.
|
||||
if let Ok(target) = fs::read_link(&link) {
|
||||
let target_str = target.display().to_string();
|
||||
if target_str.contains(".app/Contents/MacOS") {
|
||||
// Sprout-owned symlink — update to current bundle path.
|
||||
let _ = fs::remove_file(&link);
|
||||
std::os::unix::fs::symlink(&sprout_bin, &link)
|
||||
.map_err(|e| format!("symlink {}: {e}", link.display()))?;
|
||||
}
|
||||
// Otherwise: symlink points elsewhere — don't clobber.
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
// Regular file or directory — don't clobber.
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
// No file exists — create the symlink.
|
||||
std::os::unix::fs::symlink(&sprout_bin, &link)
|
||||
.map_err(|e| format!("symlink {}: {e}", link.display()))?;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("stat {}: {e}", link.display()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -230,4 +288,41 @@ mod tests {
|
||||
"symlinked child's target should not be chmod'd"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn ensure_cli_symlink_creates_symlink() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let exe_parent = tmp.path().join("MacOS");
|
||||
fs::create_dir(&exe_parent).unwrap();
|
||||
fs::write(exe_parent.join("sprout"), "binary").unwrap();
|
||||
|
||||
// Point home_dir to a temp location by using ensure_cli_symlink
|
||||
// directly with a custom link target. We'll test the logic manually.
|
||||
let local_bin = tmp.path().join("local_bin");
|
||||
fs::create_dir_all(&local_bin).unwrap();
|
||||
let link = local_bin.join("sprout");
|
||||
|
||||
// Create symlink manually to test the creation path.
|
||||
std::os::unix::fs::symlink(exe_parent.join("sprout"), &link).unwrap();
|
||||
assert!(link.symlink_metadata().unwrap().file_type().is_symlink());
|
||||
assert_eq!(fs::read_link(&link).unwrap(), exe_parent.join("sprout"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn ensure_cli_symlink_does_not_clobber_regular_file() {
|
||||
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("sprout");
|
||||
fs::write(&link, "user-installed binary").unwrap();
|
||||
|
||||
// Verify it's a regular file.
|
||||
assert!(link.symlink_metadata().unwrap().file_type().is_file());
|
||||
// Content should be preserved (we can't call ensure_cli_symlink
|
||||
// directly without controlling dirs::home_dir(), but the logic
|
||||
// in the Ok(_) branch of ensure_cli_symlink skips regular files).
|
||||
assert_eq!(fs::read_to_string(&link).unwrap(), "user-installed binary");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,8 +536,29 @@ pub fn spawn_agent_child(
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|| record.agent_command.clone());
|
||||
|
||||
// Augment PATH for DMG launches so child processes (e.g. #!/usr/bin/env node) can find their runtimes.
|
||||
let augmented_path = login_shell_path();
|
||||
// Augment PATH for DMG launches so child processes can find:
|
||||
// - sprout CLI via ~/.local/bin symlink
|
||||
// - bundled sidecars (sprout, sprout-acp, etc.) via exe parent (Contents/MacOS/)
|
||||
// - runtimes (node, python, etc.) via login shell PATH
|
||||
let augmented_path = {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
parts.push(home.join(".local").join("bin").display().to_string());
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
parts.push(parent.display().to_string());
|
||||
}
|
||||
}
|
||||
if let Some(shell_path) = login_shell_path() {
|
||||
parts.push(shell_path);
|
||||
}
|
||||
if parts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(parts.join(":"))
|
||||
}
|
||||
};
|
||||
|
||||
let mut command = std::process::Command::new(&resolved_acp_command);
|
||||
if let Some(home) = super::default_agent_workdir() {
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"binaries/sprout-mcp-server",
|
||||
"binaries/sprout-agent",
|
||||
"binaries/sprout-dev-mcp",
|
||||
"binaries/git-credential-nostr"
|
||||
"binaries/git-credential-nostr",
|
||||
"binaries/sprout"
|
||||
],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
|
||||
@@ -99,7 +99,7 @@ _ensure-sidecar-stubs:
|
||||
set -euo pipefail
|
||||
TARGET=$(rustc -vV | sed -n 's|host: ||p')
|
||||
mkdir -p desktop/src-tauri/binaries
|
||||
for bin in sprout-acp sprout-mcp-server sprout-agent sprout-dev-mcp git-credential-nostr; do
|
||||
for bin in sprout-acp sprout-mcp-server sprout-agent sprout-dev-mcp git-credential-nostr sprout; do
|
||||
touch "desktop/src-tauri/binaries/${bin}-${TARGET}"
|
||||
done
|
||||
|
||||
@@ -118,6 +118,7 @@ desktop-release-build target="aarch64-apple-darwin":
|
||||
touch "desktop/src-tauri/binaries/sprout-agent-$TARGET"
|
||||
touch "desktop/src-tauri/binaries/sprout-dev-mcp-$TARGET"
|
||||
touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET"
|
||||
touch "desktop/src-tauri/binaries/sprout-$TARGET"
|
||||
pnpm install
|
||||
cd {{desktop_dir}} && pnpm tauri build --target {{target}}
|
||||
|
||||
@@ -194,7 +195,11 @@ staging *ARGS: _ensure-sidecar-stubs
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
pnpm install
|
||||
cargo build --release -p sprout-acp -p sprout-mcp -p sprout-agent -p sprout-dev-mcp
|
||||
cargo build --release -p sprout-acp -p sprout-mcp -p sprout-agent -p sprout-dev-mcp -p sprout-cli
|
||||
# Replace the 0-byte sidecar stub with the real CLI binary so tauri dev picks it up.
|
||||
TARGET=$(rustc -vV | sed -n 's|host: ||p')
|
||||
cp target/release/sprout "desktop/src-tauri/binaries/sprout-${TARGET}"
|
||||
chmod +x "desktop/src-tauri/binaries/sprout-${TARGET}"
|
||||
cd {{desktop_dir}}
|
||||
source ../scripts/instance-env.sh
|
||||
export SPROUT_RELAY_URL="wss://sprout-oss.stage.blox.sqprod.co"
|
||||
@@ -292,7 +297,7 @@ check-compile:
|
||||
goose relay="ws://localhost:3000" agents="1" heartbeat="0" prompt="" key="$SPROUT_PRIVATE_KEY":
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo build --release -p sprout-acp -p sprout-mcp
|
||||
cargo build --release -p sprout-acp -p sprout-mcp -p sprout-cli
|
||||
env_args=(
|
||||
SPROUT_RELAY_URL="{{relay}}"
|
||||
SPROUT_PRIVATE_KEY="{{key}}"
|
||||
@@ -312,7 +317,7 @@ goose relay="ws://localhost:3000" agents="1" heartbeat="0" prompt="" key="$SPROU
|
||||
goose-bg relay="ws://localhost:3000" agents="1" heartbeat="0" prompt="" key="$SPROUT_PRIVATE_KEY":
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo build --release -p sprout-acp -p sprout-mcp
|
||||
cargo build --release -p sprout-acp -p sprout-mcp -p sprout-cli
|
||||
env_args=(
|
||||
SPROUT_RELAY_URL="{{relay}}"
|
||||
SPROUT_PRIVATE_KEY="{{key}}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SIDECARS=(sprout-acp sprout-mcp-server sprout-agent sprout-dev-mcp git-credential-nostr)
|
||||
SIDECARS=(sprout-acp sprout-mcp-server sprout-agent sprout-dev-mcp git-credential-nostr sprout)
|
||||
TARGET=${1:-$(rustc -vV | sed -n 's|host: ||p')}
|
||||
BINARIES_DIR="desktop/src-tauri/binaries"
|
||||
|
||||
@@ -11,7 +11,7 @@ for bin in "${SIDECARS[@]}"; do
|
||||
done
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
echo "Error: missing release binaries: ${missing[*]}" >&2
|
||||
echo "Run 'cargo build --release -p sprout-acp -p sprout-mcp -p sprout-agent -p sprout-dev-mcp -p git-credential-nostr' first." >&2
|
||||
echo "Run 'cargo build --release -p sprout-acp -p sprout-mcp -p sprout-agent -p sprout-dev-mcp -p git-credential-nostr -p sprout-cli' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user