commit 7b7d74ab540fb5ca68c937ff587ebb5387bea34f
parent 92386a9c250a47ea1bb2011a70cd829a8a9c6aff
Author: triesap <tyson@radroots.org>
Date: Thu, 9 Apr 2026 03:23:17 +0000
cli: extract runtime paths module
Diffstat:
3 files changed, 180 insertions(+), 144 deletions(-)
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -3,20 +3,18 @@ use std::fs;
use std::path::Path;
use std::path::PathBuf;
-use radroots_runtime_paths::{
- DEFAULT_CONFIG_FILE_NAME, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsRuntimeNamespace, default_shared_identity_path,
-};
+use radroots_runtime_paths::RadrootsPathResolver;
use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend};
use serde::Deserialize;
use url::Url;
use crate::cli::CliArgs;
use crate::runtime::RuntimeError;
+pub use crate::runtime::paths::PathsConfig;
+use crate::runtime::paths::{ENV_CLI_PATHS_PROFILE, ENV_CLI_PATHS_REPO_LOCAL_ROOT, resolve_paths};
const DEFAULT_LOG_FILTER: &str = "info";
const DEFAULT_ENV_PATH: &str = ".env";
-const DEFAULT_WORKSPACE_CONFIG_PATH: &str = ".radroots/config.toml";
const DEFAULT_LOCAL_STATE_DIR: &str = "replica";
const DEFAULT_LOCAL_DB_FILE: &str = "replica.sqlite";
const DEFAULT_LOCAL_BACKUPS_DIR: &str = "backups";
@@ -24,15 +22,9 @@ const DEFAULT_LOCAL_EXPORTS_DIR: &str = "exports";
const DEFAULT_SHARED_ACCOUNTS_STORE_FILE: &str = "store.json";
const DEFAULT_HYF_EXECUTABLE: &str = "hyfd";
const DEFAULT_RPC_URL: &str = "http://127.0.0.1:7070";
-const CLI_DEFAULT_PROFILE: &str = "interactive_user";
-const CLI_REPO_LOCAL_PROFILE: &str = "repo_local";
-const CLI_APP_NAMESPACE_VALUE: &str = "cli";
-const SHARED_ACCOUNTS_NAMESPACE_VALUE: &str = "accounts";
-const SHARED_IDENTITIES_NAMESPACE_VALUE: &str = "identities";
const CLI_HOST_VAULT_POLICY: &str = "desktop";
const CLI_DEFAULT_SECRET_BACKEND: &str = "host_vault";
const CLI_DEFAULT_SECRET_FALLBACK: &str = "encrypted_file";
-const CLI_ALLOWED_PROFILES: &[&str] = &[CLI_DEFAULT_PROFILE, CLI_REPO_LOCAL_PROFILE];
const CLI_ALLOWED_SHARED_SECRET_BACKENDS: &[&str] =
&["host_vault", "encrypted_file", "memory", "plaintext_file"];
const CLI_USES_PROTECTED_STORE: bool = true;
@@ -41,8 +33,6 @@ const ENV_OUTPUT: &str = "RADROOTS_OUTPUT";
const ENV_CLI_LOG_FILTER: &str = "RADROOTS_CLI_LOGGING_FILTER";
const ENV_CLI_LOG_DIR: &str = "RADROOTS_CLI_LOGGING_OUTPUT_DIR";
const ENV_CLI_LOG_STDOUT: &str = "RADROOTS_CLI_LOGGING_STDOUT";
-const ENV_CLI_PATHS_PROFILE: &str = "RADROOTS_CLI_PATHS_PROFILE";
-const ENV_CLI_PATHS_REPO_LOCAL_ROOT: &str = "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT";
const ENV_LOG_FILTER: &str = "RADROOTS_LOG_FILTER";
const ENV_LOG_DIR: &str = "RADROOTS_LOG_DIR";
const ENV_LOG_STDOUT: &str = "RADROOTS_LOG_STDOUT";
@@ -256,24 +246,14 @@ pub struct RuntimeConfig {
pub rpc: RpcConfig,
}
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct PathsConfig {
- pub profile: String,
- pub allowed_profiles: Vec<String>,
- pub app_namespace: String,
- pub shared_accounts_namespace: String,
- pub shared_identities_namespace: String,
- pub app_config_path: PathBuf,
- pub workspace_config_path: PathBuf,
- pub app_data_root: PathBuf,
- pub app_logs_root: PathBuf,
- pub shared_accounts_data_root: PathBuf,
- pub shared_accounts_secrets_root: PathBuf,
- pub default_identity_path: PathBuf,
-}
-
#[derive(Debug, Default)]
-struct EnvFileValues(BTreeMap<String, String>);
+pub(crate) struct EnvFileValues(BTreeMap<String, String>);
+
+impl EnvFileValues {
+ pub(crate) fn get(&self, key: &str) -> Option<&str> {
+ self.0.get(key).map(String::as_str)
+ }
+}
#[derive(Debug, Default, Deserialize)]
struct CliConfigFile {
@@ -299,7 +279,7 @@ struct HyfFileConfig {
executable: Option<PathBuf>,
}
-pub trait Environment {
+pub(crate) trait Environment {
fn var(&self, key: &str) -> Option<String>;
fn current_dir(&self) -> Result<PathBuf, RuntimeError>;
fn path_resolver(&self) -> RadrootsPathResolver;
@@ -471,119 +451,6 @@ impl RuntimeConfig {
}
}
-fn resolve_paths(
- env: &dyn Environment,
- env_file: &EnvFileValues,
-) -> Result<PathsConfig, RuntimeError> {
- let current_dir = env.current_dir()?;
- let resolver = env.path_resolver();
- let (profile, profile_label) = resolve_cli_path_profile(env, env_file)?;
- let overrides = resolve_cli_path_overrides(current_dir.as_path(), env, env_file, profile)?;
- let resolved = resolver
- .resolve(profile, &overrides)
- .map_err(|err| RuntimeError::Config(format!("resolve Radroots path roots: {err}")))?;
- let app_namespace = RadrootsRuntimeNamespace::app(CLI_APP_NAMESPACE_VALUE)
- .map_err(|err| RuntimeError::Config(format!("resolve cli namespace: {err}")))?;
- let shared_accounts_namespace =
- RadrootsRuntimeNamespace::shared(SHARED_ACCOUNTS_NAMESPACE_VALUE).map_err(|err| {
- RuntimeError::Config(format!("resolve shared accounts namespace: {err}"))
- })?;
- let shared_identity_namespace =
- RadrootsRuntimeNamespace::shared(SHARED_IDENTITIES_NAMESPACE_VALUE).map_err(|err| {
- RuntimeError::Config(format!("resolve shared identities namespace: {err}"))
- })?;
- let app_paths = resolved.namespaced(&app_namespace);
- let shared_accounts_paths = resolved.namespaced(&shared_accounts_namespace);
- let default_identity_path = default_shared_identity_path(&resolver, profile, &overrides)
- .map_err(|err| RuntimeError::Config(format!("resolve shared identity path: {err}")))?;
-
- Ok(PathsConfig {
- profile: profile_label.to_owned(),
- allowed_profiles: CLI_ALLOWED_PROFILES
- .iter()
- .map(|value| (*value).to_owned())
- .collect(),
- app_namespace: app_namespace.relative_path().display().to_string(),
- shared_accounts_namespace: shared_accounts_namespace
- .relative_path()
- .display()
- .to_string(),
- shared_identities_namespace: shared_identity_namespace
- .relative_path()
- .display()
- .to_string(),
- app_config_path: app_paths.config.join(DEFAULT_CONFIG_FILE_NAME),
- workspace_config_path: current_dir.join(DEFAULT_WORKSPACE_CONFIG_PATH),
- app_data_root: app_paths.data,
- app_logs_root: app_paths.logs,
- shared_accounts_data_root: shared_accounts_paths.data,
- shared_accounts_secrets_root: shared_accounts_paths.secrets,
- default_identity_path,
- })
-}
-
-fn resolve_cli_path_profile(
- env: &dyn Environment,
- env_file: &EnvFileValues,
-) -> Result<(RadrootsPathProfile, &'static str), RuntimeError> {
- match env_value_entry(env, env_file, &[ENV_CLI_PATHS_PROFILE]) {
- Some((key, value)) => parse_cli_path_profile(key.as_str(), value.as_str()),
- None => Ok((RadrootsPathProfile::InteractiveUser, CLI_DEFAULT_PROFILE)),
- }
-}
-
-fn parse_cli_path_profile(
- key: &str,
- value: &str,
-) -> Result<(RadrootsPathProfile, &'static str), RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- CLI_DEFAULT_PROFILE => Ok((RadrootsPathProfile::InteractiveUser, CLI_DEFAULT_PROFILE)),
- CLI_REPO_LOCAL_PROFILE => Ok((RadrootsPathProfile::RepoLocal, CLI_REPO_LOCAL_PROFILE)),
- other => Err(RuntimeError::Config(format!(
- "{key} must be `interactive_user` or `repo_local`, got `{other}`"
- ))),
- }
-}
-
-fn resolve_cli_path_overrides(
- current_dir: &Path,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- profile: RadrootsPathProfile,
-) -> Result<RadrootsPathOverrides, RuntimeError> {
- match profile {
- RadrootsPathProfile::InteractiveUser => Ok(RadrootsPathOverrides::default()),
- RadrootsPathProfile::RepoLocal => {
- let Some((key, value)) =
- env_value_entry(env, env_file, &[ENV_CLI_PATHS_REPO_LOCAL_ROOT])
- else {
- return Err(RuntimeError::Config(format!(
- "{ENV_CLI_PATHS_REPO_LOCAL_ROOT} must be set when {ENV_CLI_PATHS_PROFILE}=repo_local"
- )));
- };
- if value.trim().is_empty() {
- return Err(RuntimeError::Config(format!(
- "{key} must not be empty when {ENV_CLI_PATHS_PROFILE}=repo_local"
- )));
- }
- let repo_local_root = normalize_explicit_path_root(current_dir, value.as_str());
- Ok(RadrootsPathOverrides::repo_local(repo_local_root))
- }
- _ => Err(RuntimeError::Config(
- "cli only supports interactive_user and repo_local path profiles".to_owned(),
- )),
- }
-}
-
-fn normalize_explicit_path_root(current_dir: &Path, value: &str) -> PathBuf {
- let root = PathBuf::from(value.trim());
- if root.is_absolute() {
- root
- } else {
- current_dir.join(root)
- }
-}
-
fn load_cli_config_file(path: &Path) -> Result<Option<CliConfigFile>, RuntimeError> {
if !path.exists() {
return Ok(None);
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
@@ -10,6 +10,7 @@ pub mod logging;
pub mod myc;
pub mod network;
pub mod order;
+pub mod paths;
pub mod signer;
pub mod sync;
diff --git a/src/runtime/paths.rs b/src/runtime/paths.rs
@@ -0,0 +1,168 @@
+use std::path::{Path, PathBuf};
+
+use radroots_runtime_paths::{
+ DEFAULT_CONFIG_FILE_NAME, RadrootsPathOverrides, RadrootsPathProfile, RadrootsRuntimeNamespace,
+ default_shared_identity_path,
+};
+
+use crate::runtime::{
+ RuntimeError,
+ config::{EnvFileValues, Environment},
+};
+
+const DEFAULT_WORKSPACE_CONFIG_PATH: &str = ".radroots/config.toml";
+const CLI_DEFAULT_PROFILE: &str = "interactive_user";
+const CLI_REPO_LOCAL_PROFILE: &str = "repo_local";
+const CLI_APP_NAMESPACE_VALUE: &str = "cli";
+const SHARED_ACCOUNTS_NAMESPACE_VALUE: &str = "accounts";
+const SHARED_IDENTITIES_NAMESPACE_VALUE: &str = "identities";
+
+pub(crate) const CLI_ALLOWED_PROFILES: &[&str] = &[CLI_DEFAULT_PROFILE, CLI_REPO_LOCAL_PROFILE];
+pub(crate) const ENV_CLI_PATHS_PROFILE: &str = "RADROOTS_CLI_PATHS_PROFILE";
+pub(crate) const ENV_CLI_PATHS_REPO_LOCAL_ROOT: &str = "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT";
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PathsConfig {
+ pub profile: String,
+ pub allowed_profiles: Vec<String>,
+ pub app_namespace: String,
+ pub shared_accounts_namespace: String,
+ pub shared_identities_namespace: String,
+ pub app_config_path: PathBuf,
+ pub workspace_config_path: PathBuf,
+ pub app_data_root: PathBuf,
+ pub app_logs_root: PathBuf,
+ pub shared_accounts_data_root: PathBuf,
+ pub shared_accounts_secrets_root: PathBuf,
+ pub default_identity_path: PathBuf,
+}
+
+pub(crate) fn resolve_paths(
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+) -> Result<PathsConfig, RuntimeError> {
+ let current_dir = env.current_dir()?;
+ let resolver = env.path_resolver();
+ let (profile, profile_label) = resolve_cli_path_profile(env, env_file)?;
+ let overrides = resolve_cli_path_overrides(current_dir.as_path(), env, env_file, profile)?;
+ let resolved = resolver
+ .resolve(profile, &overrides)
+ .map_err(|err| RuntimeError::Config(format!("resolve Radroots path roots: {err}")))?;
+ let app_namespace = RadrootsRuntimeNamespace::app(CLI_APP_NAMESPACE_VALUE)
+ .map_err(|err| RuntimeError::Config(format!("resolve cli namespace: {err}")))?;
+ let shared_accounts_namespace =
+ RadrootsRuntimeNamespace::shared(SHARED_ACCOUNTS_NAMESPACE_VALUE).map_err(|err| {
+ RuntimeError::Config(format!("resolve shared accounts namespace: {err}"))
+ })?;
+ let shared_identity_namespace =
+ RadrootsRuntimeNamespace::shared(SHARED_IDENTITIES_NAMESPACE_VALUE).map_err(|err| {
+ RuntimeError::Config(format!("resolve shared identities namespace: {err}"))
+ })?;
+ let app_paths = resolved.namespaced(&app_namespace);
+ let shared_accounts_paths = resolved.namespaced(&shared_accounts_namespace);
+ let default_identity_path = default_shared_identity_path(&resolver, profile, &overrides)
+ .map_err(|err| RuntimeError::Config(format!("resolve shared identity path: {err}")))?;
+
+ Ok(PathsConfig {
+ profile: profile_label.to_owned(),
+ allowed_profiles: CLI_ALLOWED_PROFILES
+ .iter()
+ .map(|value| (*value).to_owned())
+ .collect(),
+ app_namespace: app_namespace.relative_path().display().to_string(),
+ shared_accounts_namespace: shared_accounts_namespace
+ .relative_path()
+ .display()
+ .to_string(),
+ shared_identities_namespace: shared_identity_namespace
+ .relative_path()
+ .display()
+ .to_string(),
+ app_config_path: app_paths.config.join(DEFAULT_CONFIG_FILE_NAME),
+ workspace_config_path: current_dir.join(DEFAULT_WORKSPACE_CONFIG_PATH),
+ app_data_root: app_paths.data,
+ app_logs_root: app_paths.logs,
+ shared_accounts_data_root: shared_accounts_paths.data,
+ shared_accounts_secrets_root: shared_accounts_paths.secrets,
+ default_identity_path,
+ })
+}
+
+fn resolve_cli_path_profile(
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+) -> Result<(RadrootsPathProfile, &'static str), RuntimeError> {
+ match path_env_value_entry(env, env_file, &[ENV_CLI_PATHS_PROFILE]) {
+ Some((key, value)) => parse_cli_path_profile(key.as_str(), value.as_str()),
+ None => Ok((RadrootsPathProfile::InteractiveUser, CLI_DEFAULT_PROFILE)),
+ }
+}
+
+fn parse_cli_path_profile(
+ key: &str,
+ value: &str,
+) -> Result<(RadrootsPathProfile, &'static str), RuntimeError> {
+ match value.trim().to_ascii_lowercase().as_str() {
+ CLI_DEFAULT_PROFILE => Ok((RadrootsPathProfile::InteractiveUser, CLI_DEFAULT_PROFILE)),
+ CLI_REPO_LOCAL_PROFILE => Ok((RadrootsPathProfile::RepoLocal, CLI_REPO_LOCAL_PROFILE)),
+ other => Err(RuntimeError::Config(format!(
+ "{key} must be `interactive_user` or `repo_local`, got `{other}`"
+ ))),
+ }
+}
+
+fn resolve_cli_path_overrides(
+ current_dir: &Path,
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+ profile: RadrootsPathProfile,
+) -> Result<RadrootsPathOverrides, RuntimeError> {
+ match profile {
+ RadrootsPathProfile::InteractiveUser => Ok(RadrootsPathOverrides::default()),
+ RadrootsPathProfile::RepoLocal => {
+ let Some((key, value)) =
+ path_env_value_entry(env, env_file, &[ENV_CLI_PATHS_REPO_LOCAL_ROOT])
+ else {
+ return Err(RuntimeError::Config(format!(
+ "{ENV_CLI_PATHS_REPO_LOCAL_ROOT} must be set when {ENV_CLI_PATHS_PROFILE}=repo_local"
+ )));
+ };
+ if value.trim().is_empty() {
+ return Err(RuntimeError::Config(format!(
+ "{key} must not be empty when {ENV_CLI_PATHS_PROFILE}=repo_local"
+ )));
+ }
+ Ok(RadrootsPathOverrides::repo_local(
+ normalize_explicit_path_root(current_dir, value.as_str()),
+ ))
+ }
+ _ => Err(RuntimeError::Config(
+ "cli only supports interactive_user and repo_local path profiles".to_owned(),
+ )),
+ }
+}
+
+fn normalize_explicit_path_root(current_dir: &Path, value: &str) -> PathBuf {
+ let root = PathBuf::from(value.trim());
+ if root.is_absolute() {
+ root
+ } else {
+ current_dir.join(root)
+ }
+}
+
+fn path_env_value_entry(
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+ keys: &[&str],
+) -> Option<(String, String)> {
+ keys.iter()
+ .find_map(|key| env.var(key).map(|value| ((*key).to_owned(), value)))
+ .or_else(|| {
+ keys.iter().find_map(|key| {
+ env_file
+ .get(key)
+ .map(|value| ((*key).to_owned(), value.to_owned()))
+ })
+ })
+}