app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit fdee427e98d9e384e0fbd88b098a250c4607d907
parent e3c9a2a891df00357784ac495786394ef508df9f
Author: triesap <tyson@radroots.org>
Date:   Sat, 23 May 2026 23:15:47 +0000

app: add repo-local path profile

- add explicit desktop app path profile env support
- require a repo-local root before resolving localhost app state
- keep interactive-user paths as the production default
- cover repo-local and invalid profile behavior with core tests

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 6+++---
Mcrates/shared/core/src/lib.rs | 7++++---
Mcrates/shared/core/src/paths.rs | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
3 files changed, 154 insertions(+), 26 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -3317,7 +3317,7 @@ impl DesktopAppRuntimeState { let timestamp = current_runtime_time_ms()?; let farm_d_tag = d_tag_from_uuid(saved_farm.farm_id.as_uuid()); let owner_pubkey = self.local_events_owner_pubkey(account); - let exportability = app_local_work_exportability(owner_pubkey.as_deref()); + let exportability = local_work_exportability(owner_pubkey.as_deref()); let delivery_method = projection .draft .order_methods @@ -3409,7 +3409,7 @@ impl DesktopAppRuntimeState { let listing_addr = owner_pubkey .as_ref() .map(|pubkey| format!("30402:{pubkey}:{listing_d_tag}")); - let exportability = app_local_work_exportability(owner_pubkey.as_deref()); + let exportability = local_work_exportability(owner_pubkey.as_deref()); let farm_setup = self.state_store.farm_setup_projection(); let delivery_method = farm_setup .draft @@ -3969,7 +3969,7 @@ fn is_hex_64(value: &str) -> bool { value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) } -fn app_local_work_exportability(owner_pubkey: Option<&str>) -> serde_json::Value { +fn local_work_exportability(owner_pubkey: Option<&str>) -> serde_json::Value { match owner_pubkey { Some(_) => json!({ "state": "exportable" diff --git a/crates/shared/core/src/lib.rs b/crates/shared/core/src/lib.rs @@ -17,9 +17,10 @@ pub use pack_day_export::{ prepare_pack_day_export_bundle_at_data_root, write_prepared_pack_day_export_bundle, }; pub use paths::{ - APP_RUNTIME_NAMESPACE, APP_RUNTIME_NAMESPACE_KIND, APP_RUNTIME_NAMESPACE_VALUE, - AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePathsError, AppRuntimePlatform, - AppRuntimeRoots, AppSharedAccountsPaths, AppSharedIdentityPaths, SHARED_ACCOUNTS_NAMESPACE, + APP_PATHS_PROFILE_ENV, APP_PATHS_REPO_LOCAL_ROOT_ENV, APP_RUNTIME_NAMESPACE, + APP_RUNTIME_NAMESPACE_KIND, APP_RUNTIME_NAMESPACE_VALUE, AppDesktopRuntimePaths, + AppRuntimeHostEnvironment, AppRuntimePathsError, AppRuntimePlatform, AppRuntimeRoots, + AppSharedAccountsPaths, AppSharedIdentityPaths, SHARED_ACCOUNTS_NAMESPACE, SHARED_ACCOUNTS_NAMESPACE_KIND, SHARED_ACCOUNTS_NAMESPACE_VALUE, SHARED_ACCOUNTS_STORE_FILE_NAME, SHARED_IDENTITIES_NAMESPACE, SHARED_IDENTITIES_NAMESPACE_KIND, SHARED_IDENTITIES_NAMESPACE_VALUE, SHARED_IDENTITY_FILE_NAME, diff --git a/crates/shared/core/src/paths.rs b/crates/shared/core/src/paths.rs @@ -16,6 +16,11 @@ pub const SHARED_IDENTITIES_NAMESPACE_KIND: &str = "shared"; pub const SHARED_IDENTITIES_NAMESPACE_VALUE: &str = "identities"; pub const SHARED_IDENTITIES_NAMESPACE: &str = "shared/identities"; pub const SHARED_IDENTITY_FILE_NAME: &str = "default.json"; +pub const APP_PATHS_PROFILE_ENV: &str = "RADROOTS_APP_PATHS_PROFILE"; +pub const APP_PATHS_REPO_LOCAL_ROOT_ENV: &str = "RADROOTS_APP_PATHS_REPO_LOCAL_ROOT"; + +const APP_INTERACTIVE_USER_PROFILE: &str = "interactive_user"; +const APP_REPO_LOCAL_PROFILE: &str = "repo_local"; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum AppRuntimePlatform { @@ -50,6 +55,8 @@ pub struct AppRuntimeHostEnvironment { pub home_dir: Option<PathBuf>, pub appdata_dir: Option<PathBuf>, pub localappdata_dir: Option<PathBuf>, + pub paths_profile: Option<String>, + pub repo_local_root: Option<PathBuf>, } impl AppRuntimeHostEnvironment { @@ -58,6 +65,8 @@ impl AppRuntimeHostEnvironment { home_dir: env::var_os("HOME").map(PathBuf::from), appdata_dir: env::var_os("APPDATA").map(PathBuf::from), localappdata_dir: env::var_os("LOCALAPPDATA").map(PathBuf::from), + paths_profile: env::var(APP_PATHS_PROFILE_ENV).ok(), + repo_local_root: env::var_os(APP_PATHS_REPO_LOCAL_ROOT_ENV).map(PathBuf::from), } } } @@ -170,43 +179,85 @@ fn resolve_desktop_base_roots( platform: AppRuntimePlatform, host_environment: AppRuntimeHostEnvironment, ) -> Result<AppRuntimeRoots, AppRuntimePathsError> { - let roots = match platform { + let roots = match resolve_desktop_profile(host_environment.paths_profile.as_deref())? { + AppDesktopPathProfile::InteractiveUser => resolve_interactive_user_roots( + platform, + host_environment.home_dir, + host_environment.appdata_dir, + host_environment.localappdata_dir, + )?, + AppDesktopPathProfile::RepoLocal => { + let repo_local_root = host_environment + .repo_local_root + .ok_or(AppRuntimePathsError::MissingRepoLocalRoot)?; + if repo_local_root.as_os_str().is_empty() { + return Err(AppRuntimePathsError::EmptyRepoLocalRoot); + } + AppRuntimeRoots::from_base_root(repo_local_root) + } + }; + + Ok(roots) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum AppDesktopPathProfile { + InteractiveUser, + RepoLocal, +} + +fn resolve_desktop_profile( + profile: Option<&str>, +) -> Result<AppDesktopPathProfile, AppRuntimePathsError> { + match profile { + None => Ok(AppDesktopPathProfile::InteractiveUser), + Some(value) => match value.trim().to_ascii_lowercase().as_str() { + APP_INTERACTIVE_USER_PROFILE => Ok(AppDesktopPathProfile::InteractiveUser), + APP_REPO_LOCAL_PROFILE => Ok(AppDesktopPathProfile::RepoLocal), + _ => Err(AppRuntimePathsError::UnsupportedPathProfile { + value: value.to_owned(), + }), + }, + } +} + +fn resolve_interactive_user_roots( + platform: AppRuntimePlatform, + home_dir: Option<PathBuf>, + appdata_dir: Option<PathBuf>, + localappdata_dir: Option<PathBuf>, +) -> Result<AppRuntimeRoots, AppRuntimePathsError> { + match platform { AppRuntimePlatform::Linux | AppRuntimePlatform::Macos => { - let home_dir = host_environment - .home_dir - .ok_or(AppRuntimePathsError::MissingHomeDir { platform })?; - AppRuntimeRoots::from_base_root(home_dir.join(".radroots")) + let home_dir = home_dir.ok_or(AppRuntimePathsError::MissingHomeDir { platform })?; + Ok(AppRuntimeRoots::from_base_root(home_dir.join(".radroots"))) } AppRuntimePlatform::Windows => { - let appdata_dir = host_environment - .appdata_dir - .ok_or(AppRuntimePathsError::MissingWindowsUserDirs)?; - let localappdata_dir = host_environment - .localappdata_dir - .ok_or(AppRuntimePathsError::MissingWindowsUserDirs)?; + let appdata_dir = appdata_dir.ok_or(AppRuntimePathsError::MissingWindowsUserDirs)?; + let localappdata_dir = + localappdata_dir.ok_or(AppRuntimePathsError::MissingWindowsUserDirs)?; let config_root = appdata_dir.join("Radroots"); let local_root = localappdata_dir.join("Radroots"); - AppRuntimeRoots { + Ok(AppRuntimeRoots { config: config_root.join("config"), data: local_root.join("data"), cache: local_root.join("cache"), logs: local_root.join("logs"), run: local_root.join("run"), secrets: config_root.join("secrets"), - } - } - AppRuntimePlatform::Other(_) => { - return Err(AppRuntimePathsError::UnsupportedPlatform { platform }); + }) } - }; - - Ok(roots) + AppRuntimePlatform::Other(_) => Err(AppRuntimePathsError::UnsupportedPlatform { platform }), + } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum AppRuntimePathsError { MissingHomeDir { platform: AppRuntimePlatform }, MissingWindowsUserDirs, + MissingRepoLocalRoot, + EmptyRepoLocalRoot, + UnsupportedPathProfile { value: String }, UnsupportedPlatform { platform: AppRuntimePlatform }, } @@ -222,6 +273,18 @@ impl fmt::Display for AppRuntimePathsError { } Self::MissingWindowsUserDirs => formatter .write_str("desktop runtime roots require APPDATA and LOCALAPPDATA on windows"), + Self::MissingRepoLocalRoot => write!( + formatter, + "desktop runtime roots require {APP_PATHS_REPO_LOCAL_ROOT_ENV} when {APP_PATHS_PROFILE_ENV}=repo_local" + ), + Self::EmptyRepoLocalRoot => write!( + formatter, + "{APP_PATHS_REPO_LOCAL_ROOT_ENV} must not be empty when {APP_PATHS_PROFILE_ENV}=repo_local" + ), + Self::UnsupportedPathProfile { value } => write!( + formatter, + "{APP_PATHS_PROFILE_ENV} must be `interactive_user` or `repo_local`, got `{value}`" + ), Self::UnsupportedPlatform { platform } => write!( formatter, "desktop runtime roots are unsupported on {}", @@ -334,6 +397,70 @@ mod tests { } #[test] + fn desktop_runtime_roots_use_explicit_repo_local_root() { + let paths = AppDesktopRuntimePaths::for_desktop( + AppRuntimePlatform::Macos, + AppRuntimeHostEnvironment { + paths_profile: Some("repo_local".to_owned()), + repo_local_root: Some(PathBuf::from("/repo/infra/local/runtime/radroots")), + ..AppRuntimeHostEnvironment::default() + }, + ) + .expect("repo-local roots should resolve"); + + assert_eq!( + paths.app.data, + PathBuf::from("/repo/infra/local/runtime/radroots/data/apps/app") + ); + assert_eq!( + paths.app.logs, + PathBuf::from("/repo/infra/local/runtime/radroots/logs/apps/app") + ); + assert_eq!( + paths.shared_accounts.data_root, + PathBuf::from("/repo/infra/local/runtime/radroots/data/shared/accounts") + ); + assert_eq!( + paths.shared_identity.default_identity_path, + PathBuf::from("/repo/infra/local/runtime/radroots/secrets/shared/identities") + .join(SHARED_IDENTITY_FILE_NAME) + ); + } + + #[test] + fn repo_local_profile_requires_explicit_root() { + let err = AppRuntimeRoots::for_desktop( + AppRuntimePlatform::Macos, + AppRuntimeHostEnvironment { + paths_profile: Some("repo_local".to_owned()), + ..AppRuntimeHostEnvironment::default() + }, + ) + .expect_err("repo-local root should be required"); + + assert_eq!(err, AppRuntimePathsError::MissingRepoLocalRoot); + } + + #[test] + fn unsupported_path_profile_is_rejected() { + let err = AppRuntimeRoots::for_desktop( + AppRuntimePlatform::Macos, + AppRuntimeHostEnvironment { + paths_profile: Some("dev".to_owned()), + ..AppRuntimeHostEnvironment::default() + }, + ) + .expect_err("unsupported profile should fail"); + + assert_eq!( + err, + AppRuntimePathsError::UnsupportedPathProfile { + value: "dev".to_owned(), + } + ); + } + + #[test] fn desktop_runtime_roots_require_home_dir_on_unix() { let err = AppRuntimeRoots::for_desktop( AppRuntimePlatform::Macos,