goose2 distribution bundling (#8911)

This commit is contained in:
Jack Amadeo
2026-05-05 11:20:49 -04:00
committed by GitHub
parent fbb5e3685d
commit 9f3fe88afa
25 changed files with 589 additions and 29 deletions
@@ -0,0 +1,7 @@
use crate::services::distro_bundle::{DistroBundleInfo, DistroBundleState};
use tauri::State;
#[tauri::command]
pub fn get_distro_bundle(state: State<'_, DistroBundleState>) -> DistroBundleInfo {
state.info()
}
+1
View File
@@ -1,6 +1,7 @@
pub mod acp;
pub mod agent_setup;
pub mod agents;
pub mod distro;
pub mod doctor;
pub mod git;
pub mod git_changes;
+7 -1
View File
@@ -2,7 +2,9 @@ mod commands;
mod services;
mod types;
use services::distro_bundle::DistroBundleState;
use services::personas::PersonaStore;
use tauri::Manager;
use tauri_plugin_window_state::StateFlags;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -30,6 +32,10 @@ pub fn run() {
let builder = builder.plugin(tauri_plugin_app_test_driver::init());
builder
.setup(|app| {
app.manage(DistroBundleState::new(app.handle()));
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::agents::list_personas,
commands::agents::create_persona,
@@ -72,6 +78,7 @@ pub fn run() {
commands::agent_setup::install_agent,
commands::agent_setup::authenticate_agent,
commands::path_resolver::resolve_path,
commands::distro::get_distro_bundle,
commands::system::get_home_dir,
commands::system::save_exported_session_file,
commands::system::path_exists,
@@ -80,7 +87,6 @@ pub fn run() {
commands::system::list_files_for_mentions,
commands::system::read_image_attachment,
])
.setup(|_app| Ok(()))
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|_app, _event| {});
@@ -1,8 +1,12 @@
use tauri::Manager;
use tauri_plugin_shell::ShellExt;
use std::ffi::OsString;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use crate::services::distro_bundle::DistroBundleState;
use tokio::process::{Child, Command};
use tokio::sync::OnceCell;
@@ -68,6 +72,18 @@ impl GooseServeProcess {
let mut command: Command = get_goose_command(&app_handle)?;
let binary_display = command.as_std().get_program().to_string_lossy().to_string();
if let Some(distro_state) = app_handle.try_state::<DistroBundleState>() {
if let Some(bundle) = distro_state.bundle() {
if let Some(bin_dir) = &bundle.bin_dir {
prepend_path_env(&mut command, bin_dir);
}
if let Some(config_path) = &bundle.config_path {
append_additional_config_env(&mut command, config_path);
}
command.env("GOOSE_DISTRO_DIR", &bundle.root_dir);
}
}
command
.arg("serve")
.arg("--host")
@@ -149,6 +165,54 @@ fn default_serve_working_dir() -> PathBuf {
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"))
}
fn prepend_path_env(command: &mut Command, extra_dir: &std::path::Path) {
let mut paths = vec![extra_dir.to_path_buf()];
if let Some(existing) = std::env::var_os("PATH") {
paths.extend(std::env::split_paths(&existing));
}
set_path_list_env(command, "PATH", paths, Some(extra_dir.as_os_str()));
}
fn append_additional_config_env(command: &mut Command, config_path: &std::path::Path) {
let existing = std::env::var_os("GOOSE_ADDITIONAL_CONFIG_FILES");
let mut paths: Vec<PathBuf> = existing
.as_ref()
.map(std::env::split_paths)
.map(Iterator::collect)
.unwrap_or_default();
paths.push(config_path.to_path_buf());
if let Ok(joined) = std::env::join_paths(&paths) {
command.env("GOOSE_ADDITIONAL_CONFIG_FILES", joined);
} else {
let mut fallback = existing.unwrap_or_default();
if !fallback.is_empty() {
fallback.push(if cfg!(windows) { ";" } else { ":" });
}
fallback.push(config_path.as_os_str());
command.env("GOOSE_ADDITIONAL_CONFIG_FILES", fallback);
}
}
fn set_path_list_env(
command: &mut Command,
key: &str,
paths: Vec<PathBuf>,
fallback_prefix: Option<&std::ffi::OsStr>,
) {
if let Ok(joined) = std::env::join_paths(&paths) {
command.env(key, joined);
} else if let Some(prefix) = fallback_prefix {
let mut fallback = OsString::from(prefix);
for path in paths.iter().skip(1) {
fallback.push(if cfg!(windows) { ";" } else { ":" });
fallback.push(path.as_os_str());
}
command.env(key, fallback);
}
}
fn reserve_free_port() -> Result<u16, String> {
let listener = std::net::TcpListener::bind((LOCALHOST, 0))
.map_err(|error| format!("Failed to reserve Goose serve port: {error}"))?;
@@ -0,0 +1,164 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager};
const DISTRO_DIR_NAME: &str = "distro";
const DISTRO_JSON_NAME: &str = "distro.json";
const DISTRO_CONFIG_NAME: &str = "config.yaml";
const DISTRO_BIN_DIR_NAME: &str = "bin";
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DistroManifest {
pub app_version: Option<String>,
pub feature_toggles: Option<HashMap<String, bool>>,
pub extension_allowlist: Option<String>,
pub provider_allowlist: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DistroBundleInfo {
pub present: bool,
pub app_version: Option<String>,
pub feature_toggles: Option<HashMap<String, bool>>,
pub extension_allowlist: Option<String>,
pub provider_allowlist: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DistroBundle {
pub root_dir: PathBuf,
pub config_path: Option<PathBuf>,
pub bin_dir: Option<PathBuf>,
pub manifest: DistroManifest,
}
pub struct DistroBundleState {
bundle: Option<DistroBundle>,
}
impl DistroBundleState {
pub fn new(app_handle: &AppHandle) -> Self {
let bundle = load_distro_bundle(app_handle)
.map_err(|error| {
log::warn!("Failed to load distro bundle: {error}");
error
})
.ok()
.flatten();
Self { bundle }
}
pub fn info(&self) -> DistroBundleInfo {
let Some(bundle) = &self.bundle else {
return DistroBundleInfo {
present: false,
app_version: None,
feature_toggles: None,
extension_allowlist: None,
provider_allowlist: None,
};
};
DistroBundleInfo {
present: true,
app_version: bundle.manifest.app_version.clone(),
feature_toggles: bundle.manifest.feature_toggles.clone(),
extension_allowlist: bundle.manifest.extension_allowlist.clone(),
provider_allowlist: bundle.manifest.provider_allowlist.clone(),
}
}
pub fn bundle(&self) -> Option<&DistroBundle> {
self.bundle.as_ref()
}
}
fn load_distro_bundle(app_handle: &AppHandle) -> Result<Option<DistroBundle>, String> {
let Some(root_dir) = resolve_distro_root(app_handle)? else {
return Ok(None);
};
let manifest_path = root_dir.join(DISTRO_JSON_NAME);
if !manifest_path.exists() {
return Ok(None);
}
let manifest = read_manifest(&manifest_path)?;
let config_path = root_dir.join(DISTRO_CONFIG_NAME);
let bin_dir = root_dir.join(DISTRO_BIN_DIR_NAME);
Ok(Some(DistroBundle {
root_dir,
config_path: config_path.exists().then_some(config_path),
bin_dir: bin_dir.is_dir().then_some(bin_dir),
manifest,
}))
}
fn resolve_distro_root(app_handle: &AppHandle) -> Result<Option<PathBuf>, String> {
if let Ok(override_dir) = env::var("GOOSE_DISTRO_DIR") {
let path = PathBuf::from(override_dir);
if path.is_dir() {
return Ok(Some(path));
}
return Err(format!(
"GOOSE_DISTRO_DIR points to a non-directory path: {}",
path.display()
));
}
let resource_dir = app_handle
.path()
.resource_dir()
.map_err(|error| format!("Failed to resolve Tauri resource dir: {error}"))?;
let distro_dir = resource_dir.join(DISTRO_DIR_NAME);
Ok(distro_dir.is_dir().then_some(distro_dir))
}
fn read_manifest(path: &Path) -> Result<DistroManifest, String> {
let contents = std::fs::read_to_string(path).map_err(|error| {
format!(
"Failed to read distro manifest '{}': {error}",
path.display()
)
})?;
serde_json::from_str::<DistroManifest>(&contents).map_err(|error| {
format!(
"Failed to parse distro manifest '{}': {error}",
path.display()
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_partial_manifest() {
let manifest = serde_json::from_str::<DistroManifest>(
r#"{
"appVersion": "development",
"featureToggles": {"foo": true}
}"#,
)
.expect("manifest should parse");
assert_eq!(manifest.app_version.as_deref(), Some("development"));
assert_eq!(
manifest
.feature_toggles
.as_ref()
.and_then(|toggles| toggles.get("foo"))
.copied(),
Some(true)
);
}
}
+1
View File
@@ -1,2 +1,3 @@
pub mod acp;
pub mod distro_bundle;
pub mod personas;