goose2 distribution bundling (#8911)
This commit is contained in:
@@ -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,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;
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
pub mod acp;
|
||||
pub mod distro_bundle;
|
||||
pub mod personas;
|
||||
|
||||
Reference in New Issue
Block a user