commit 3a292865c8260e4b15731c224a25eb754be0ad0d
parent d1cf7fd498936e7e177732dc4afa5dcf8997df2a
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Apr 2026 23:38:16 +0000
paths: adopt interactive-user defaults in cli
Diffstat:
15 files changed, 427 insertions(+), 137 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1658,6 +1658,7 @@ dependencies = [
"radroots-protected-store",
"radroots-replica-db",
"radroots-replica-sync",
+ "radroots-runtime-paths",
"radroots-secret-vault",
"radroots-sql-core",
"radroots-trade",
@@ -1708,6 +1709,7 @@ dependencies = [
"nostr",
"radroots-events",
"radroots-runtime",
+ "radroots-runtime-paths",
"serde",
"serde_json",
"thiserror 1.0.69",
@@ -1829,6 +1831,7 @@ dependencies = [
"getrandom 0.2.17",
"radroots-log",
"radroots-protected-store",
+ "radroots-runtime-paths",
"radroots-secret-vault",
"serde",
"serde_json",
@@ -1841,6 +1844,13 @@ dependencies = [
]
[[package]]
+name = "radroots-runtime-paths"
+version = "0.1.0-alpha.1"
+dependencies = [
+ "thiserror 1.0.69",
+]
+
+[[package]]
name = "radroots-secret-vault"
version = "0.1.0-alpha.1"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
@@ -31,6 +31,7 @@ radroots-nostr-signer = { path = "../lib/crates/nostr-signer" }
radroots-protected-store = { path = "../lib/crates/protected-store", features = ["std"] }
radroots-replica-db = { path = "../lib/crates/replica-db" }
radroots-replica-sync = { path = "../lib/crates/replica-sync" }
+radroots-runtime-paths = { path = "../lib/crates/runtime-paths" }
radroots-secret-vault = { path = "../lib/crates/secret-vault", features = ["std", "os-keyring"] }
radroots-sql-core = { path = "../lib/crates/sql-core", features = ["native"] }
radroots-trade = { path = "../lib/crates/trade" }
diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs
@@ -90,13 +90,13 @@ pub fn report(
fn config_check(config: &RuntimeConfig) -> EvaluatedCheck {
let detail = match (
- config.paths.user_config_path.exists(),
+ config.paths.app_config_path.exists(),
config.paths.workspace_config_path.exists(),
) {
(false, false) => "defaults active".to_owned(),
- (true, false) => "user config root present".to_owned(),
+ (true, false) => "app config root present".to_owned(),
(false, true) => "workspace config root present".to_owned(),
- (true, true) => "user and workspace config roots present".to_owned(),
+ (true, true) => "app and workspace config roots present".to_owned(),
};
EvaluatedCheck {
diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs
@@ -21,13 +21,22 @@ pub fn show(
dry_run: config.output.dry_run,
},
config_files: ConfigFilesRuntimeView {
- user_present: config.paths.user_config_path.exists(),
+ user_present: config.paths.app_config_path.exists(),
workspace_present: config.paths.workspace_config_path.exists(),
},
paths: PathsRuntimeView {
- user_config_path: config.paths.user_config_path.display().to_string(),
+ profile: config.paths.profile.clone(),
+ app_config_path: config.paths.app_config_path.display().to_string(),
workspace_config_path: config.paths.workspace_config_path.display().to_string(),
- user_state_root: config.paths.user_state_root.display().to_string(),
+ app_data_root: config.paths.app_data_root.display().to_string(),
+ app_logs_root: config.paths.app_logs_root.display().to_string(),
+ shared_accounts_data_root: config.paths.shared_accounts_data_root.display().to_string(),
+ shared_accounts_secrets_root: config
+ .paths
+ .shared_accounts_secrets_root
+ .display()
+ .to_string(),
+ default_identity_path: config.paths.default_identity_path.display().to_string(),
},
logging: LoggingRuntimeView {
initialized: logging.initialized,
@@ -47,7 +56,7 @@ pub fn show(
selector: config.account.selector.clone(),
store_path: config.account.store_path.display().to_string(),
secrets_dir: config.account.secrets_dir.display().to_string(),
- legacy_identity_path: config.identity.path.display().to_string(),
+ identity_path: config.identity.path.display().to_string(),
secret_backend: AccountSecretRuntimeView {
configured_primary: secret_backend.configured_primary,
configured_fallback: secret_backend.configured_fallback,
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -145,9 +145,14 @@ pub struct LoggingRuntimeView {
#[derive(Debug, Clone, Serialize)]
pub struct PathsRuntimeView {
- pub user_config_path: String,
+ pub profile: String,
+ pub app_config_path: String,
pub workspace_config_path: String,
- pub user_state_root: String,
+ pub app_data_root: String,
+ pub app_logs_root: String,
+ pub shared_accounts_data_root: String,
+ pub shared_accounts_secrets_root: String,
+ pub default_identity_path: String,
}
#[derive(Debug, Clone, Serialize)]
@@ -156,7 +161,7 @@ pub struct AccountRuntimeView {
pub selector: Option<String>,
pub store_path: String,
pub secrets_dir: String,
- pub legacy_identity_path: String,
+ pub identity_path: String,
pub secret_backend: AccountSecretRuntimeView,
}
diff --git a/src/render/mod.rs b/src/render/mod.rs
@@ -489,7 +489,7 @@ fn render_config_show(
let user_config = format!(
"{} · {}",
present_absent(view.config_files.user_present),
- view.paths.user_config_path
+ view.paths.app_config_path
);
let workspace_config = format!(
"{} · {}",
@@ -500,9 +500,23 @@ fn render_config_show(
stdout,
"config roots",
&[
- ("user config", user_config.as_str()),
+ ("profile", view.paths.profile.as_str()),
+ ("app config", user_config.as_str()),
("workspace config", workspace_config.as_str()),
- ("user state root", view.paths.user_state_root.as_str()),
+ ("app data root", view.paths.app_data_root.as_str()),
+ ("app logs root", view.paths.app_logs_root.as_str()),
+ (
+ "shared accounts data",
+ view.paths.shared_accounts_data_root.as_str(),
+ ),
+ (
+ "shared accounts secrets",
+ view.paths.shared_accounts_secrets_root.as_str(),
+ ),
+ (
+ "default identity path",
+ view.paths.default_identity_path.as_str(),
+ ),
],
)?;
@@ -525,10 +539,7 @@ fn render_config_show(
"secret backend",
view.account.secret_backend.configured_primary.as_str(),
),
- (
- "legacy import path",
- view.account.legacy_identity_path.as_str(),
- ),
+ ("identity path", view.account.identity_path.as_str()),
];
if let Some(fallback) = &view.account.secret_backend.configured_fallback {
account_rows.push(("secret fallback", fallback.as_str()));
@@ -2066,9 +2077,16 @@ mod tests {
dry_run: false,
},
paths: PathsConfig {
- user_config_path: "/home/tester/.config/radroots/config.toml".into(),
+ profile: "interactive_user".into(),
+ app_config_path: "/home/tester/.radroots/config/apps/cli/config.toml".into(),
workspace_config_path: "/workspace/.radroots/config.toml".into(),
- user_state_root: "/home/tester/.local/share/radroots".into(),
+ app_data_root: "/home/tester/.radroots/data/apps/cli".into(),
+ app_logs_root: "/home/tester/.radroots/logs/apps/cli".into(),
+ shared_accounts_data_root: "/home/tester/.radroots/data/shared/accounts".into(),
+ shared_accounts_secrets_root: "/home/tester/.radroots/secrets/shared/accounts"
+ .into(),
+ default_identity_path:
+ "/home/tester/.radroots/secrets/shared/identities/default.json".into(),
},
logging: LoggingConfig {
filter: "info".to_owned(),
@@ -2077,13 +2095,13 @@ mod tests {
},
account: AccountConfig {
selector: Some("acct_demo".into()),
- store_path: "/home/tester/.local/share/radroots/accounts/store.json".into(),
- secrets_dir: "/home/tester/.local/share/radroots/accounts/secrets".into(),
+ store_path: "/home/tester/.radroots/data/shared/accounts/store.json".into(),
+ secrets_dir: "/home/tester/.radroots/secrets/shared/accounts".into(),
secret_backend: RadrootsSecretBackend::EncryptedFile,
secret_fallback: None,
},
identity: IdentityConfig {
- path: "identity.json".into(),
+ path: "/home/tester/.radroots/secrets/shared/identities/default.json".into(),
},
signer: SignerConfig {
backend: SignerBackend::Local,
@@ -2094,11 +2112,11 @@ mod tests {
source: RelayConfigSource::WorkspaceConfig,
},
local: LocalConfig {
- root: "/home/tester/.local/share/radroots/replica".into(),
- replica_db_path: "/home/tester/.local/share/radroots/replica/replica.sqlite"
+ root: "/home/tester/.radroots/data/apps/cli/replica".into(),
+ replica_db_path: "/home/tester/.radroots/data/apps/cli/replica/replica.sqlite"
.into(),
- backups_dir: "/home/tester/.local/share/radroots/replica/backups".into(),
- exports_dir: "/home/tester/.local/share/radroots/replica/exports".into(),
+ backups_dir: "/home/tester/.radroots/data/apps/cli/replica/backups".into(),
+ exports_dir: "/home/tester/.radroots/data/apps/cli/replica/exports".into(),
},
myc: MycConfig {
executable: "myc".into(),
@@ -2115,6 +2133,7 @@ mod tests {
)
.expect("runtime show");
assert_eq!(view.output.format, "human");
+ assert_eq!(view.paths.profile, "interactive_user");
assert_eq!(
view.paths.workspace_config_path,
"/workspace/.radroots/config.toml"
@@ -2123,14 +2142,14 @@ mod tests {
assert!(
view.account
.store_path
- .ends_with(".local/share/radroots/accounts/store.json")
+ .ends_with(".radroots/data/shared/accounts/store.json")
);
assert_eq!(view.relay.count, 2);
assert_eq!(view.relay.publish_policy, "any");
assert!(
view.local
.replica_db_path
- .ends_with(".local/share/radroots/replica/replica.sqlite")
+ .ends_with(".radroots/data/apps/cli/replica/replica.sqlite")
);
}
@@ -2167,9 +2186,18 @@ mod tests {
dry_run: true,
},
paths: PathsConfig {
- user_config_path: "/home/tester/.config/radroots/config.toml".into(),
+ profile: "interactive_user".into(),
+ app_config_path: "/home/tester/.radroots/config/apps/cli/config.toml"
+ .into(),
workspace_config_path: "/workspace/.radroots/config.toml".into(),
- user_state_root: "/home/tester/.local/share/radroots".into(),
+ app_data_root: "/home/tester/.radroots/data/apps/cli".into(),
+ app_logs_root: "/home/tester/.radroots/logs/apps/cli".into(),
+ shared_accounts_data_root: "/home/tester/.radroots/data/shared/accounts"
+ .into(),
+ shared_accounts_secrets_root:
+ "/home/tester/.radroots/secrets/shared/accounts".into(),
+ default_identity_path:
+ "/home/tester/.radroots/secrets/shared/identities/default.json".into(),
},
logging: LoggingConfig {
filter: "info".to_owned(),
@@ -2178,13 +2206,14 @@ mod tests {
},
account: AccountConfig {
selector: None,
- store_path: "/home/tester/.local/share/radroots/accounts/store.json".into(),
- secrets_dir: "/home/tester/.local/share/radroots/accounts/secrets".into(),
+ store_path: "/home/tester/.radroots/data/shared/accounts/store.json".into(),
+ secrets_dir: "/home/tester/.radroots/secrets/shared/accounts".into(),
secret_backend: RadrootsSecretBackend::EncryptedFile,
secret_fallback: None,
},
identity: IdentityConfig {
- path: "identity.json".into(),
+ path: "/home/tester/.radroots/secrets/shared/identities/default.json"
+ .into(),
},
signer: SignerConfig {
backend: SignerBackend::Local,
@@ -2195,11 +2224,11 @@ mod tests {
source: RelayConfigSource::Defaults,
},
local: LocalConfig {
- root: "/home/tester/.local/share/radroots/replica".into(),
+ root: "/home/tester/.radroots/data/apps/cli/replica".into(),
replica_db_path:
- "/home/tester/.local/share/radroots/replica/replica.sqlite".into(),
- backups_dir: "/home/tester/.local/share/radroots/replica/backups".into(),
- exports_dir: "/home/tester/.local/share/radroots/replica/exports".into(),
+ "/home/tester/.radroots/data/apps/cli/replica/replica.sqlite".into(),
+ backups_dir: "/home/tester/.radroots/data/apps/cli/replica/backups".into(),
+ exports_dir: "/home/tester/.radroots/data/apps/cli/replica/exports".into(),
},
myc: MycConfig {
executable: "myc".into(),
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -3,6 +3,11 @@ use std::fs;
use std::path::Path;
use std::path::PathBuf;
+use radroots_identity::DEFAULT_IDENTITY_PATH;
+use radroots_runtime_paths::{
+ DEFAULT_CONFIG_FILE_NAME, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsRuntimeNamespace,
+};
use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend};
use serde::Deserialize;
use url::Url;
@@ -13,12 +18,11 @@ use crate::runtime::RuntimeError;
const DEFAULT_LOG_FILTER: &str = "info";
const DEFAULT_ENV_PATH: &str = ".env";
const DEFAULT_WORKSPACE_CONFIG_PATH: &str = ".radroots/config.toml";
-const DEFAULT_USER_CONFIG_PATH: &str = ".config/radroots/config.toml";
-const DEFAULT_USER_STATE_ROOT: &str = ".local/share/radroots";
const DEFAULT_LOCAL_STATE_DIR: &str = "replica";
const DEFAULT_LOCAL_DB_FILE: &str = "replica.sqlite";
const DEFAULT_LOCAL_BACKUPS_DIR: &str = "backups";
const DEFAULT_LOCAL_EXPORTS_DIR: &str = "exports";
+const DEFAULT_SHARED_ACCOUNTS_STORE_FILE: &str = "store.json";
const DEFAULT_RPC_URL: &str = "http://127.0.0.1:7070";
const ENV_FILE_PATH: &str = "RADROOTS_ENV_FILE";
const ENV_OUTPUT: &str = "RADROOTS_OUTPUT";
@@ -217,9 +221,14 @@ pub struct RuntimeConfig {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathsConfig {
- pub user_config_path: PathBuf,
+ pub profile: String,
+ pub app_config_path: PathBuf,
pub workspace_config_path: PathBuf,
- pub user_state_root: 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)]
@@ -245,7 +254,7 @@ struct RpcFileConfig {
pub trait Environment {
fn var(&self, key: &str) -> Option<String>;
fn current_dir(&self) -> Result<PathBuf, RuntimeError>;
- fn home_dir(&self) -> Option<PathBuf>;
+ fn path_resolver(&self) -> RadrootsPathResolver;
}
pub struct SystemEnvironment;
@@ -261,8 +270,8 @@ impl Environment for SystemEnvironment {
})
}
- fn home_dir(&self) -> Option<PathBuf> {
- std::env::var_os("HOME").map(PathBuf::from)
+ fn path_resolver(&self) -> RadrootsPathResolver {
+ RadrootsPathResolver::current()
}
}
@@ -281,7 +290,7 @@ impl RuntimeConfig {
) -> Result<Self, RuntimeError> {
let paths = resolve_paths(env)?;
let workspace_config = load_cli_config_file(paths.workspace_config_path.as_path())?;
- let user_config = load_cli_config_file(paths.user_config_path.as_path())?;
+ let app_config = load_cli_config_file(paths.app_config_path.as_path())?;
let account_secret_backend = resolve_account_secret_backend(args, env, env_file)?
.unwrap_or(RadrootsSecretBackend::HostVault(
RadrootsHostVaultPolicy::desktop(),
@@ -306,7 +315,9 @@ impl RuntimeConfig {
.or_else(|| env_value(env, env_file, &[ENV_CLI_LOG_FILTER, ENV_LOG_FILTER]))
.unwrap_or_else(|| DEFAULT_LOG_FILTER.to_owned()),
directory: args.log_dir.clone().or_else(|| {
- env_value(env, env_file, &[ENV_CLI_LOG_DIR, ENV_LOG_DIR]).map(PathBuf::from)
+ env_value(env, env_file, &[ENV_CLI_LOG_DIR, ENV_LOG_DIR])
+ .map(PathBuf::from)
+ .or_else(|| Some(paths.app_logs_root.clone()))
}),
stdout: resolve_bool_pair(
args.log_stdout,
@@ -324,8 +335,10 @@ impl RuntimeConfig {
.account
.clone()
.or_else(|| env_value(env, env_file, &[ENV_ACCOUNT])),
- store_path: paths.user_state_root.join("accounts/store.json"),
- secrets_dir: paths.user_state_root.join("accounts/secrets"),
+ store_path: paths
+ .shared_accounts_data_root
+ .join(DEFAULT_SHARED_ACCOUNTS_STORE_FILE),
+ secrets_dir: paths.shared_accounts_secrets_root.clone(),
secret_backend: account_secret_backend,
secret_fallback: account_secret_fallback,
},
@@ -334,7 +347,7 @@ impl RuntimeConfig {
.identity_path
.clone()
.or_else(|| env_value(env, env_file, &[ENV_IDENTITY_PATH]).map(PathBuf::from))
- .unwrap_or_else(|| PathBuf::from("identity.json")),
+ .unwrap_or_else(|| paths.default_identity_path.clone()),
},
signer: SignerConfig {
backend: args
@@ -349,21 +362,21 @@ impl RuntimeConfig {
args,
env,
env_file,
- user_config.as_ref(),
+ app_config.as_ref(),
workspace_config.as_ref(),
)?,
local: LocalConfig {
- root: paths.user_state_root.join(DEFAULT_LOCAL_STATE_DIR),
+ root: paths.app_data_root.join(DEFAULT_LOCAL_STATE_DIR),
replica_db_path: paths
- .user_state_root
+ .app_data_root
.join(DEFAULT_LOCAL_STATE_DIR)
.join(DEFAULT_LOCAL_DB_FILE),
backups_dir: paths
- .user_state_root
+ .app_data_root
.join(DEFAULT_LOCAL_STATE_DIR)
.join(DEFAULT_LOCAL_BACKUPS_DIR),
exports_dir: paths
- .user_state_root
+ .app_data_root
.join(DEFAULT_LOCAL_STATE_DIR)
.join(DEFAULT_LOCAL_EXPORTS_DIR),
},
@@ -377,7 +390,7 @@ impl RuntimeConfig {
rpc: resolve_rpc_config(
env,
env_file,
- user_config.as_ref(),
+ app_config.as_ref(),
workspace_config.as_ref(),
)?,
})
@@ -386,16 +399,34 @@ impl RuntimeConfig {
fn resolve_paths(env: &dyn Environment) -> Result<PathsConfig, RuntimeError> {
let current_dir = env.current_dir()?;
- let home_dir = env.home_dir().ok_or_else(|| {
- RuntimeError::Config(
- "failed to resolve home directory for Radroots config roots".to_owned(),
+ let resolver = env.path_resolver();
+ let resolved = resolver
+ .resolve(
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
)
- })?;
+ .map_err(|err| RuntimeError::Config(format!("resolve Radroots path roots: {err}")))?;
+ let app_namespace = RadrootsRuntimeNamespace::app("cli")
+ .map_err(|err| RuntimeError::Config(format!("resolve cli namespace: {err}")))?;
+ let shared_accounts_namespace = RadrootsRuntimeNamespace::shared("accounts")
+ .map_err(|err| RuntimeError::Config(format!("resolve shared accounts namespace: {err}")))?;
+ let app_paths = resolved.namespaced(&app_namespace);
+ let shared_accounts_paths = resolved.namespaced(&shared_accounts_namespace);
+ let default_identity_path = resolved
+ .secrets
+ .join("shared")
+ .join("identities")
+ .join(DEFAULT_IDENTITY_PATH);
Ok(PathsConfig {
- user_config_path: home_dir.join(DEFAULT_USER_CONFIG_PATH),
+ profile: RadrootsPathProfile::InteractiveUser.to_string(),
+ app_config_path: app_paths.config.join(DEFAULT_CONFIG_FILE_NAME),
workspace_config_path: current_dir.join(DEFAULT_WORKSPACE_CONFIG_PATH),
- user_state_root: home_dir.join(DEFAULT_USER_STATE_ROOT),
+ 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,
})
}
@@ -842,6 +873,7 @@ mod tests {
};
use crate::cli::CliArgs;
use clap::Parser;
+ use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform};
use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend};
use std::collections::BTreeMap;
use std::fs;
@@ -851,7 +883,7 @@ mod tests {
struct MapEnvironment {
values: BTreeMap<String, String>,
current_dir: PathBuf,
- home_dir: PathBuf,
+ path_resolver: RadrootsPathResolver,
}
impl MapEnvironment {
@@ -859,7 +891,13 @@ mod tests {
Self {
values,
current_dir: PathBuf::from("/workspaces/radroots-cli"),
- home_dir: PathBuf::from("/home/tester"),
+ path_resolver: RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/tester")),
+ ..RadrootsHostEnvironment::default()
+ },
+ ),
}
}
}
@@ -873,8 +911,8 @@ mod tests {
Ok(self.current_dir.clone())
}
- fn home_dir(&self) -> Option<PathBuf> {
- Some(self.home_dir.clone())
+ fn path_resolver(&self) -> RadrootsPathResolver {
+ self.path_resolver.clone()
}
}
@@ -929,11 +967,24 @@ mod tests {
assert_eq!(
resolved.paths,
PathsConfig {
- user_config_path: PathBuf::from("/home/tester/.config/radroots/config.toml"),
+ profile: "interactive_user".to_owned(),
+ app_config_path: PathBuf::from(
+ "/home/tester/.radroots/config/apps/cli/config.toml"
+ ),
workspace_config_path: PathBuf::from(
"/workspaces/radroots-cli/.radroots/config.toml"
),
- user_state_root: PathBuf::from("/home/tester/.local/share/radroots"),
+ app_data_root: PathBuf::from("/home/tester/.radroots/data/apps/cli"),
+ app_logs_root: PathBuf::from("/home/tester/.radroots/logs/apps/cli"),
+ shared_accounts_data_root: PathBuf::from(
+ "/home/tester/.radroots/data/shared/accounts"
+ ),
+ shared_accounts_secrets_root: PathBuf::from(
+ "/home/tester/.radroots/secrets/shared/accounts"
+ ),
+ default_identity_path: PathBuf::from(
+ "/home/tester/.radroots/secrets/shared/identities/default.json"
+ ),
}
);
assert_eq!(resolved.logging.filter, "debug");
@@ -946,8 +997,8 @@ mod tests {
resolved.account,
AccountConfig {
selector: None,
- store_path: PathBuf::from("/home/tester/.local/share/radroots/accounts/store.json"),
- secrets_dir: PathBuf::from("/home/tester/.local/share/radroots/accounts/secrets"),
+ store_path: PathBuf::from("/home/tester/.radroots/data/shared/accounts/store.json"),
+ secrets_dir: PathBuf::from("/home/tester/.radroots/secrets/shared/accounts"),
secret_backend: RadrootsSecretBackend::HostVault(
RadrootsHostVaultPolicy::desktop(),
),
@@ -1146,14 +1197,14 @@ RADROOTS_CLI_LOGGING_STDOUT=false
let workspace_root = temp.path().join("workspace");
let user_home = temp.path().join("home");
fs::create_dir_all(workspace_root.join(".radroots")).expect("workspace config dir");
- fs::create_dir_all(user_home.join(".config/radroots")).expect("user config dir");
+ fs::create_dir_all(user_home.join(".radroots/config/apps/cli")).expect("app config dir");
fs::write(
workspace_root.join(".radroots/config.toml"),
"[relay]\nurls = [\"wss://relay.workspace\"]\npublish_policy = \"any\"\n",
)
.expect("write workspace config");
fs::write(
- user_home.join(".config/radroots/config.toml"),
+ user_home.join(".radroots/config/apps/cli/config.toml"),
"[relay]\nurls = [\"wss://relay.user\", \"wss://relay.workspace\"]\n",
)
.expect("write user config");
@@ -1161,7 +1212,13 @@ RADROOTS_CLI_LOGGING_STDOUT=false
let env = MapEnvironment {
values: BTreeMap::new(),
current_dir: workspace_root,
- home_dir: user_home,
+ path_resolver: RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(user_home),
+ ..RadrootsHostEnvironment::default()
+ },
+ ),
};
let args = CliArgs::parse_from(["radroots", "config", "show"]);
@@ -1201,16 +1258,71 @@ RADROOTS_CLI_LOGGING_STDOUT=false
.expect("resolve runtime config");
assert_eq!(
- resolved.paths.user_config_path,
- PathBuf::from("/home/tester/.config/radroots/config.toml")
+ resolved.paths.app_config_path,
+ PathBuf::from("/home/tester/.radroots/config/apps/cli/config.toml")
);
assert_eq!(
resolved.paths.workspace_config_path,
PathBuf::from("/workspaces/radroots-cli/.radroots/config.toml")
);
assert_eq!(
- resolved.paths.user_state_root,
- PathBuf::from("/home/tester/.local/share/radroots")
+ resolved.paths.app_data_root,
+ PathBuf::from("/home/tester/.radroots/data/apps/cli")
+ );
+ }
+
+ #[test]
+ fn windows_roots_use_native_user_directories() {
+ let args = CliArgs::parse_from(["radroots", "config", "show"]);
+ let env = MapEnvironment {
+ values: BTreeMap::new(),
+ current_dir: PathBuf::from(r"C:\workspaces\radroots-cli"),
+ path_resolver: RadrootsPathResolver::new(
+ RadrootsPlatform::Windows,
+ RadrootsHostEnvironment {
+ appdata_dir: Some(PathBuf::from(r"C:\Users\tester\AppData\Roaming")),
+ localappdata_dir: Some(PathBuf::from(r"C:\Users\tester\AppData\Local")),
+ ..RadrootsHostEnvironment::default()
+ },
+ ),
+ };
+
+ let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
+ .expect("resolve runtime config");
+
+ assert_eq!(
+ resolved.paths.app_config_path,
+ PathBuf::from(r"C:\Users\tester\AppData\Roaming")
+ .join("Radroots")
+ .join("config")
+ .join("apps")
+ .join("cli")
+ .join("config.toml")
+ );
+ assert_eq!(
+ resolved.paths.app_data_root,
+ PathBuf::from(r"C:\Users\tester\AppData\Local")
+ .join("Radroots")
+ .join("data")
+ .join("apps")
+ .join("cli")
+ );
+ assert_eq!(
+ resolved.paths.shared_accounts_data_root,
+ PathBuf::from(r"C:\Users\tester\AppData\Local")
+ .join("Radroots")
+ .join("data")
+ .join("shared")
+ .join("accounts")
+ );
+ assert_eq!(
+ resolved.paths.default_identity_path,
+ PathBuf::from(r"C:\Users\tester\AppData\Roaming")
+ .join("Radroots")
+ .join("secrets")
+ .join("shared")
+ .join("identities")
+ .join("default.json")
);
}
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -1210,7 +1210,7 @@ fn scaffold_contents(draft: &OrderDraftDocument) -> Result<String, RuntimeError>
}
fn drafts_dir(config: &RuntimeConfig) -> PathBuf {
- config.paths.user_state_root.join(ORDERS_DIR)
+ config.paths.app_data_root.join(ORDERS_DIR)
}
fn draft_lookup_path(config: &RuntimeConfig, lookup: &str) -> PathBuf {
diff --git a/tests/find.rs b/tests/find.rs
@@ -6,10 +6,20 @@ use radroots_sql_core::{SqlExecutor, SqliteExecutor};
use serde_json::{Value, json};
use tempfile::tempdir;
+fn data_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ workdir.join("local").join("Radroots").join("data")
+ } else {
+ workdir.join("home").join(".radroots").join("data")
+ }
+}
+
fn cli_command_in(workdir: &Path) -> Command {
let mut command = Command::cargo_bin("radroots").expect("binary");
command.current_dir(workdir);
command.env("HOME", workdir.join("home"));
+ command.env("APPDATA", workdir.join("roaming"));
+ command.env("LOCALAPPDATA", workdir.join("local"));
for key in [
"RADROOTS_ENV_FILE",
"RADROOTS_OUTPUT",
@@ -178,9 +188,7 @@ fn seed_trade_product(
qty_avail: i64,
location_label: Option<&str>,
) {
- let replica_db = workdir
- .join("home")
- .join(".local/share/radroots/replica/replica.sqlite");
+ let replica_db = data_root(workdir).join("apps/cli/replica/replica.sqlite");
let executor = SqliteExecutor::open(&replica_db).expect("open replica db");
let now = "2026-04-07T00:00:00.000Z";
executor
diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs
@@ -6,10 +6,28 @@ use assert_cmd::prelude::*;
use serde_json::Value;
use tempfile::tempdir;
+fn data_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ workdir.join("local").join("Radroots").join("data")
+ } else {
+ workdir.join("home").join(".radroots").join("data")
+ }
+}
+
+fn secrets_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ workdir.join("roaming").join("Radroots").join("secrets")
+ } else {
+ workdir.join("home").join(".radroots").join("secrets")
+ }
+}
+
fn cli_command_in(workdir: &Path) -> Command {
let mut command = Command::cargo_bin("radroots").expect("binary");
command.current_dir(workdir);
command.env("HOME", workdir.join("home"));
+ command.env("APPDATA", workdir.join("roaming"));
+ command.env("LOCALAPPDATA", workdir.join("local"));
for key in [
"RADROOTS_ENV_FILE",
"RADROOTS_OUTPUT",
@@ -39,9 +57,7 @@ fn cli_command_in(workdir: &Path) -> Command {
#[test]
fn account_new_json_creates_local_account_store_entry() {
let dir = tempdir().expect("tempdir");
- let store_path = dir
- .path()
- .join("home/.local/share/radroots/accounts/store.json");
+ let store_path = data_root(dir.path()).join("shared/accounts/store.json");
let output = cli_command_in(dir.path())
.args(["--json", "account", "new"])
@@ -75,9 +91,7 @@ fn account_new_encrypts_file_backed_secret_fallback_by_default() {
assert!(output.status.success());
let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
let account_id = json["account"]["id"].as_str().expect("account id");
- let secrets_dir = dir
- .path()
- .join("home/.local/share/radroots/accounts/secrets");
+ let secrets_dir = secrets_root(dir.path()).join("shared/accounts");
let envelope_path = secrets_dir.join(format!("{account_id}.secret.json"));
assert!(secrets_dir.join(".vault.key").exists());
@@ -98,9 +112,7 @@ fn account_new_encrypts_file_backed_secret_fallback_by_default() {
#[test]
fn account_new_rejects_dry_run_without_creating_store_state() {
let dir = tempdir().expect("tempdir");
- let store_path = dir
- .path()
- .join("home/.local/share/radroots/accounts/store.json");
+ let store_path = data_root(dir.path()).join("shared/accounts/store.json");
let output = cli_command_in(dir.path())
.args(["--dry-run", "account", "new"])
diff --git a/tests/listing.rs b/tests/listing.rs
@@ -13,10 +13,20 @@ use radroots_sql_core::{SqlExecutor, SqliteExecutor};
use serde_json::{Value, json};
use tempfile::tempdir;
+fn data_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ workdir.join("local").join("Radroots").join("data")
+ } else {
+ workdir.join("home").join(".radroots").join("data")
+ }
+}
+
fn cli_command_in(workdir: &Path) -> Command {
let mut command = Command::cargo_bin("radroots").expect("binary");
command.current_dir(workdir);
command.env("HOME", workdir.join("home"));
+ command.env("APPDATA", workdir.join("roaming"));
+ command.env("LOCALAPPDATA", workdir.join("local"));
for key in [
"RADROOTS_ENV_FILE",
"RADROOTS_OUTPUT",
@@ -468,9 +478,7 @@ fn listing_archive_and_dry_run_are_truthful() {
}
fn seed_farm(workdir: &Path, pubkey: &str, d_tag: &str, name: &str) {
- let replica_db = workdir
- .join("home")
- .join(".local/share/radroots/replica/replica.sqlite");
+ let replica_db = data_root(workdir).join("apps/cli/replica/replica.sqlite");
let executor = SqliteExecutor::open(&replica_db).expect("open replica db");
let now = "2026-04-07T00:00:00.000Z";
executor
@@ -724,9 +732,7 @@ fn seed_trade_product(
qty_avail: i64,
location_label: Option<&str>,
) {
- let replica_db = workdir
- .join("home")
- .join(".local/share/radroots/replica/replica.sqlite");
+ let replica_db = data_root(workdir).join("apps/cli/replica/replica.sqlite");
let executor = SqliteExecutor::open(&replica_db).expect("open replica db");
let now = "2026-04-07T00:00:00.000Z";
executor
diff --git a/tests/local.rs b/tests/local.rs
@@ -6,10 +6,20 @@ use assert_cmd::prelude::*;
use serde_json::Value;
use tempfile::tempdir;
+fn data_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ workdir.join("local").join("Radroots").join("data")
+ } else {
+ workdir.join("home").join(".radroots").join("data")
+ }
+}
+
fn local_command_in(workdir: &Path) -> Command {
let mut command = Command::cargo_bin("radroots").expect("binary");
command.current_dir(workdir);
command.env("HOME", workdir.join("home"));
+ command.env("APPDATA", workdir.join("roaming"));
+ command.env("LOCALAPPDATA", workdir.join("local"));
for key in [
"RADROOTS_ENV_FILE",
"RADROOTS_OUTPUT",
@@ -49,21 +59,16 @@ fn local_init_json_creates_replica_db_and_roots() {
assert_eq!(json["state"], "initialized");
assert_eq!(json["replica_db"], "ready");
- let replica_db = dir
- .path()
- .join("home")
- .join(".local/share/radroots/replica/replica.sqlite");
+ let replica_db = data_root(dir.path()).join("apps/cli/replica/replica.sqlite");
assert!(replica_db.exists());
assert!(
- dir.path()
- .join("home")
- .join(".local/share/radroots/replica/backups")
+ data_root(dir.path())
+ .join("apps/cli/replica/backups")
.exists()
);
assert!(
- dir.path()
- .join("home")
- .join(".local/share/radroots/replica/exports")
+ data_root(dir.path())
+ .join("apps/cli/replica/exports")
.exists()
);
}
diff --git a/tests/order.rs b/tests/order.rs
@@ -12,10 +12,20 @@ use assert_cmd::prelude::*;
use serde_json::Value;
use tempfile::tempdir;
+fn data_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ workdir.join("local").join("Radroots").join("data")
+ } else {
+ workdir.join("home").join(".radroots").join("data")
+ }
+}
+
fn order_command_in(workdir: &Path) -> Command {
let mut command = Command::cargo_bin("radroots").expect("binary");
command.current_dir(workdir);
command.env("HOME", workdir.join("home"));
+ command.env("APPDATA", workdir.join("roaming"));
+ command.env("LOCALAPPDATA", workdir.join("local"));
for key in [
"RADROOTS_ENV_FILE",
"RADROOTS_OUTPUT",
@@ -310,7 +320,7 @@ fn order_new_creates_a_local_draft_with_selected_account_defaults() {
assert_eq!(json["items"][0]["bin_count"], 2);
let file = json["file"].as_str().expect("draft file");
- assert!(file.contains(".local/share/radroots/orders/drafts/ord_"));
+ assert!(file.contains("/data/apps/cli/orders/drafts/ord_"));
let contents = fs::read_to_string(file).expect("read order draft");
assert!(contents.contains("kind = \"order_draft_v1\""));
assert!(contents.contains("listing_lookup = \"pasture-eggs\""));
@@ -398,7 +408,7 @@ fn order_get_and_ls_read_local_drafts_and_report_missing() {
fn order_get_surfaces_recorded_job_metadata_from_the_local_draft_store() {
let _guard = order_test_guard();
let dir = tempdir().expect("tempdir");
- let drafts_dir = dir.path().join("home/.local/share/radroots/orders/drafts");
+ let drafts_dir = data_root(dir.path()).join("apps/cli/orders/drafts");
fs::create_dir_all(&drafts_dir).expect("create drafts dir");
let draft_path = drafts_dir.join("ord_AAAAAAAAAAAAAAAAAAAAAg.toml");
fs::write(
@@ -544,7 +554,7 @@ fn order_watch_reports_job_frames_for_submitted_order() {
other => panic!("unexpected mock rpc method {other}"),
});
- let drafts_dir = dir.path().join("home/.local/share/radroots/orders/drafts");
+ let drafts_dir = data_root(dir.path()).join("apps/cli/orders/drafts");
fs::create_dir_all(&drafts_dir).expect("create drafts dir");
fs::write(
drafts_dir.join("ord_AAAAAAAAAAAAAAAAAAAAAg.toml"),
@@ -596,7 +606,7 @@ job_id = "job_watch_01"
fn order_history_lists_submitted_order_drafts() {
let _guard = order_test_guard();
let dir = tempdir().expect("tempdir");
- let drafts_dir = dir.path().join("home/.local/share/radroots/orders/drafts");
+ let drafts_dir = data_root(dir.path()).join("apps/cli/orders/drafts");
fs::create_dir_all(&drafts_dir).expect("create drafts dir");
fs::write(
drafts_dir.join("ord_AAAAAAAAAAAAAAAAAAAAAg.toml"),
@@ -649,7 +659,7 @@ submitted_at_unix = 1712720000
fn order_cancel_is_truthfully_narrowed_when_trade_chain_state_is_unavailable() {
let _guard = order_test_guard();
let dir = tempdir().expect("tempdir");
- let drafts_dir = dir.path().join("home/.local/share/radroots/orders/drafts");
+ let drafts_dir = data_root(dir.path()).join("apps/cli/orders/drafts");
fs::create_dir_all(&drafts_dir).expect("create drafts dir");
fs::write(
drafts_dir.join("ord_AAAAAAAAAAAAAAAAAAAAAg.toml"),
diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs
@@ -6,10 +6,60 @@ use assert_cmd::prelude::*;
use serde_json::Value;
use tempfile::tempdir;
+fn appdata_root(workdir: &Path) -> std::path::PathBuf {
+ workdir.join("roaming").join("Radroots")
+}
+
+fn localappdata_root(workdir: &Path) -> std::path::PathBuf {
+ workdir.join("local").join("Radroots")
+}
+
+fn interactive_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ localappdata_root(workdir)
+ } else {
+ workdir.join("home").join(".radroots")
+ }
+}
+
+fn config_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ appdata_root(workdir).join("config")
+ } else {
+ interactive_root(workdir).join("config")
+ }
+}
+
+fn data_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ localappdata_root(workdir).join("data")
+ } else {
+ interactive_root(workdir).join("data")
+ }
+}
+
+fn logs_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ localappdata_root(workdir).join("logs")
+ } else {
+ interactive_root(workdir).join("logs")
+ }
+}
+
+fn secrets_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ appdata_root(workdir).join("secrets")
+ } else {
+ interactive_root(workdir).join("secrets")
+ }
+}
+
fn runtime_show_command_in(workdir: &Path) -> Command {
let mut command = Command::cargo_bin("radroots").expect("binary");
command.current_dir(workdir);
command.env("HOME", workdir.join("home"));
+ command.env("APPDATA", workdir.join("roaming"));
+ command.env("LOCALAPPDATA", workdir.join("local"));
for key in [
"RADROOTS_ENV_FILE",
"RADROOTS_OUTPUT",
@@ -53,11 +103,11 @@ fn config_show_json_reports_default_bootstrap_state() {
assert_eq!(json["output"]["verbosity"], "normal");
assert_eq!(json["output"]["color"], true);
assert_eq!(json["output"]["dry_run"], false);
+ assert_eq!(json["paths"]["profile"], "interactive_user");
assert_eq!(
- json["paths"]["user_config_path"],
- dir.path()
- .join("home")
- .join(".config/radroots/config.toml")
+ json["paths"]["app_config_path"],
+ config_root(dir.path())
+ .join("apps/cli/config.toml")
.display()
.to_string()
);
@@ -69,36 +119,64 @@ fn config_show_json_reports_default_bootstrap_state() {
.to_string()
);
assert_eq!(
- json["paths"]["user_state_root"],
- dir.path()
- .join("home")
- .join(".local/share/radroots")
+ json["paths"]["app_data_root"],
+ data_root(dir.path()).join("apps/cli").display().to_string()
+ );
+ assert_eq!(
+ json["paths"]["app_logs_root"],
+ logs_root(dir.path()).join("apps/cli").display().to_string()
+ );
+ assert_eq!(
+ json["paths"]["shared_accounts_data_root"],
+ data_root(dir.path())
+ .join("shared/accounts")
+ .display()
+ .to_string()
+ );
+ assert_eq!(
+ json["paths"]["shared_accounts_secrets_root"],
+ secrets_root(dir.path())
+ .join("shared/accounts")
+ .display()
+ .to_string()
+ );
+ assert_eq!(
+ json["paths"]["default_identity_path"],
+ secrets_root(dir.path())
+ .join("shared/identities/default.json")
.display()
.to_string()
);
assert_eq!(json["logging"]["initialized"], true);
assert_eq!(json["logging"]["stdout"], false);
- assert_eq!(json["logging"]["directory"], Value::Null);
+ assert_eq!(
+ json["logging"]["directory"],
+ logs_root(dir.path()).join("apps/cli").display().to_string()
+ );
assert_eq!(json["config_files"]["user_present"], false);
assert_eq!(json["config_files"]["workspace_present"], false);
assert_eq!(json["account"]["selector"], Value::Null);
assert_eq!(
json["account"]["store_path"],
- dir.path()
- .join("home")
- .join(".local/share/radroots/accounts/store.json")
+ data_root(dir.path())
+ .join("shared/accounts/store.json")
.display()
.to_string()
);
assert_eq!(
json["account"]["secrets_dir"],
- dir.path()
- .join("home")
- .join(".local/share/radroots/accounts/secrets")
+ secrets_root(dir.path())
+ .join("shared/accounts")
+ .display()
+ .to_string()
+ );
+ assert_eq!(
+ json["account"]["identity_path"],
+ secrets_root(dir.path())
+ .join("shared/identities/default.json")
.display()
.to_string()
);
- assert_eq!(json["account"]["legacy_identity_path"], "identity.json");
assert_eq!(
json["account"]["secret_backend"]["configured_primary"],
"host_vault"
@@ -119,17 +197,15 @@ fn config_show_json_reports_default_bootstrap_state() {
assert_eq!(json["relay"]["source"], "defaults · local first");
assert_eq!(
json["local"]["root"],
- dir.path()
- .join("home")
- .join(".local/share/radroots/replica")
+ data_root(dir.path())
+ .join("apps/cli/replica")
.display()
.to_string()
);
assert_eq!(
json["local"]["replica_db_path"],
- dir.path()
- .join("home")
- .join(".local/share/radroots/replica/replica.sqlite")
+ data_root(dir.path())
+ .join("apps/cli/replica/replica.sqlite")
.display()
.to_string()
);
@@ -164,10 +240,7 @@ fn config_show_json_reflects_environment_configuration() {
assert_eq!(json["logging"]["filter"], "debug");
assert_eq!(json["logging"]["directory"], "logs/runtime");
assert_eq!(json["account"]["selector"], "acct_demo");
- assert_eq!(
- json["account"]["legacy_identity_path"],
- "state/identity.json"
- );
+ assert_eq!(json["account"]["identity_path"], "state/identity.json");
assert_eq!(
json["account"]["secret_backend"]["active_backend"],
"encrypted_file"
diff --git a/tests/signer_status.rs b/tests/signer_status.rs
@@ -6,10 +6,20 @@ use assert_cmd::prelude::*;
use serde_json::Value;
use tempfile::tempdir;
+fn data_root(workdir: &Path) -> std::path::PathBuf {
+ if cfg!(windows) {
+ workdir.join("local").join("Radroots").join("data")
+ } else {
+ workdir.join("home").join(".radroots").join("data")
+ }
+}
+
fn cli_command_in(workdir: &Path) -> Command {
let mut command = Command::cargo_bin("radroots").expect("binary");
command.current_dir(workdir);
command.env("HOME", workdir.join("home"));
+ command.env("APPDATA", workdir.join("roaming"));
+ command.env("LOCALAPPDATA", workdir.join("local"));
for key in [
"RADROOTS_ENV_FILE",
"RADROOTS_OUTPUT",
@@ -90,7 +100,7 @@ fn signer_status_reports_local_unconfigured_when_no_account_is_selected() {
#[test]
fn signer_status_reports_internal_error_for_invalid_account_store_file() {
let dir = tempdir().expect("tempdir");
- let accounts_dir = dir.path().join("home/.local/share/radroots/accounts");
+ let accounts_dir = data_root(dir.path()).join("shared/accounts");
fs::create_dir_all(&accounts_dir).expect("create accounts dir");
fs::write(accounts_dir.join("store.json"), "{ not valid json").expect("write invalid store");