Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 19 additions & 19 deletions desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[workspace]

[package]
name = "sprout"
name = "sprout-desktop"
version = "0.1.0"
description = "Sprout desktop app"
authors = ["you"]
Expand Down
10 changes: 10 additions & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
95 changes: 95 additions & 0 deletions desktop/src-tauri/src/managed_agents/nest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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");
}
}
25 changes: 23 additions & 2 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
3 changes: 2 additions & 1 deletion desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 9 additions & 4 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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}}

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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}}"
Expand All @@ -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}}"
Expand Down
4 changes: 2 additions & 2 deletions scripts/bundle-sidecars.sh
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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

Expand Down
Loading