cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit cf21782c9a1ff6ffddc79ed0b71937e357b37a85
parent ecbd9d0c8e3a323402d62dc178aafabaef97857f
Author: triesap <tyson@radroots.org>
Date:   Sat, 25 Apr 2026 06:19:26 +0000

runtime: make workspace config repo-local only

Diffstat:
Msrc/commands/doctor.rs | 6+++++-
Msrc/commands/runtime.rs | 13+++++++++++--
Msrc/domain/runtime.rs | 3++-
Msrc/render/mod.rs | 17++++++++---------
Msrc/runtime/config.rs | 24++++++++++++++----------
Msrc/runtime/farm_config.rs | 18+++++++++++++-----
Msrc/runtime/paths.rs | 13+++++--------
Msrc/runtime/provider.rs | 4+---
Mtests/runtime_show.rs | 12++++--------
9 files changed, 63 insertions(+), 47 deletions(-)

diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs @@ -95,7 +95,11 @@ pub fn report( fn config_check(config: &RuntimeConfig) -> EvaluatedCheck { let detail = match ( config.paths.app_config_path.exists(), - config.paths.workspace_config_path.exists(), + config + .paths + .workspace_config_path + .as_ref() + .is_some_and(|path| path.exists()), ) { (false, false) => "defaults active".to_owned(), (true, false) => "app config root present".to_owned(), diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs @@ -46,7 +46,11 @@ pub fn show( }, config_files: ConfigFilesRuntimeView { user_present: config.paths.app_config_path.exists(), - workspace_present: config.paths.workspace_config_path.exists(), + workspace_present: config + .paths + .workspace_config_path + .as_ref() + .is_some_and(|path| path.exists()), }, paths: PathsRuntimeView { profile: config.paths.profile.clone(), @@ -64,7 +68,12 @@ pub fn show( shared_accounts_namespace: config.paths.shared_accounts_namespace.clone(), shared_identities_namespace: config.paths.shared_identities_namespace.clone(), app_config_path: config.paths.app_config_path.display().to_string(), - workspace_config_path: config.paths.workspace_config_path.display().to_string(), + workspace_config_enabled: config.paths.workspace_config_path.is_some(), + workspace_config_path: config + .paths + .workspace_config_path + .as_ref() + .map(|path| path.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(), diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs @@ -348,7 +348,8 @@ pub struct PathsRuntimeView { pub shared_accounts_namespace: String, pub shared_identities_namespace: String, pub app_config_path: String, - pub workspace_config_path: String, + pub workspace_config_enabled: bool, + pub workspace_config_path: Option<String>, pub app_data_root: String, pub app_logs_root: String, pub shared_accounts_data_root: String, diff --git a/src/render/mod.rs b/src/render/mod.rs @@ -1129,7 +1129,10 @@ fn render_config_show( let workspace_config = format!( "{} ยท {}", present_absent(view.config_files.workspace_present), - view.paths.workspace_config_path + view.paths + .workspace_config_path + .as_deref() + .unwrap_or("disabled for interactive_user") ); let allowed_profiles = view.paths.allowed_profiles.join(", "); render_pairs( @@ -4334,8 +4337,7 @@ mod tests { shared_accounts_namespace: "shared/accounts".into(), shared_identities_namespace: "shared/identities".into(), app_config_path: "/home/tester/.radroots/config/apps/cli/config.toml".into(), - workspace_config_path: "/workspace/infra/local/runtime/radroots/config.toml" - .into(), + workspace_config_path: None, 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(), @@ -4409,10 +4411,8 @@ mod tests { assert_eq!(view.paths.profile, "interactive_user"); assert_eq!(view.paths.app_namespace, "apps/cli"); assert_eq!(view.paths.shared_accounts_namespace, "shared/accounts"); - assert_eq!( - view.paths.workspace_config_path, - "/workspace/infra/local/runtime/radroots/config.toml" - ); + assert!(!view.paths.workspace_config_enabled); + assert_eq!(view.paths.workspace_config_path, None); assert_eq!(view.account.selector.as_deref(), Some("acct_demo")); assert!( view.account @@ -4490,8 +4490,7 @@ mod tests { shared_identities_namespace: "shared/identities".into(), app_config_path: "/home/tester/.radroots/config/apps/cli/config.toml" .into(), - workspace_config_path: - "/workspace/infra/local/runtime/radroots/config.toml".into(), + workspace_config_path: None, 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" diff --git a/src/runtime/config.rs b/src/runtime/config.rs @@ -467,7 +467,12 @@ impl RuntimeConfig { ) -> Result<Self, RuntimeError> { let paths = resolve_paths(env, env_file)?; let migration = resolve_migration(paths.clone(), env); - let workspace_config = load_cli_config_file(paths.workspace_config_path.as_path())?; + let workspace_config = paths + .workspace_config_path + .as_deref() + .map(load_cli_config_file) + .transpose()? + .flatten(); 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( @@ -1521,9 +1526,7 @@ mod tests { app_config_path: PathBuf::from( "/home/tester/.radroots/config/apps/cli/config.toml" ), - workspace_config_path: PathBuf::from( - "/workspaces/radroots-cli/infra/local/runtime/radroots/config.toml" - ), + workspace_config_path: None, 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( @@ -2164,10 +2167,7 @@ target = "https://rpc.workspace.test/jsonrpc" resolved.paths.shared_identities_namespace, "shared/identities" ); - assert_eq!( - resolved.paths.workspace_config_path, - PathBuf::from("/workspaces/radroots-cli/infra/local/runtime/radroots/config.toml") - ); + assert_eq!(resolved.paths.workspace_config_path, None); assert_eq!( resolved.paths.app_data_root, PathBuf::from("/home/tester/.radroots/data/apps/cli") @@ -2276,7 +2276,9 @@ target = "https://rpc.workspace.test/jsonrpc" ); assert_eq!( resolved.paths.workspace_config_path, - PathBuf::from("/workspaces/radroots-cli/.local/radroots/dev/config.toml") + Some(PathBuf::from( + "/workspaces/radroots-cli/.local/radroots/dev/config.toml" + )) ); assert_eq!( resolved.paths.app_data_root, @@ -2337,7 +2339,9 @@ RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT=.local/radroots/dev ); assert_eq!( resolved.paths.workspace_config_path, - PathBuf::from("/workspaces/radroots-cli/.local/radroots/dev/config.toml") + Some(PathBuf::from( + "/workspaces/radroots-cli/.local/radroots/dev/config.toml" + )) ); } diff --git a/src/runtime/farm_config.rs b/src/runtime/farm_config.rs @@ -128,10 +128,16 @@ pub fn user_config_path(paths: &PathsConfig) -> Result<PathBuf, RuntimeError> { } pub fn workspace_config_path(paths: &PathsConfig) -> Result<PathBuf, RuntimeError> { - let Some(parent) = paths.workspace_config_path.parent() else { + let Some(path) = paths.workspace_config_path.as_ref() else { + return Err(RuntimeError::Config(format!( + "workspace farm config requires repo_local path profile, got `{}`", + paths.profile + ))); + }; + let Some(parent) = path.parent() else { return Err(RuntimeError::Config(format!( "workspace config path {} has no parent directory", - paths.workspace_config_path.display() + path.display() ))); }; Ok(parent.join("config/apps/cli").join(FARM_CONFIG_FILE_NAME)) @@ -350,19 +356,21 @@ mod tests { use tempfile::tempdir; fn sample_paths(profile: &str, root: &Path) -> PathsConfig { + let repo_local_root = root.join("infra/local/runtime/radroots"); PathsConfig { profile: profile.to_owned(), profile_source: "test".to_owned(), allowed_profiles: vec!["interactive_user".to_owned(), "repo_local".to_owned()], root_source: "test".to_owned(), - repo_local_root: Some(root.join("infra/local/runtime/radroots")), + repo_local_root: Some(repo_local_root.clone()), repo_local_root_source: Some("test".to_owned()), subordinate_path_override_source: "test".to_owned(), app_namespace: "apps/cli".to_owned(), shared_accounts_namespace: "shared/accounts".to_owned(), shared_identities_namespace: "shared/identities".to_owned(), app_config_path: root.join("home/.radroots/config/apps/cli/config.toml"), - workspace_config_path: root.join("workspace/infra/local/runtime/radroots/config.toml"), + workspace_config_path: (profile == "repo_local") + .then(|| repo_local_root.join("config.toml")), app_data_root: root.join("home/.radroots/data/apps/cli"), app_logs_root: root.join("home/.radroots/logs/apps/cli"), shared_accounts_data_root: root.join("home/.radroots/data/shared/accounts"), @@ -463,7 +471,7 @@ mod tests { let paths = sample_paths("repo_local", dir.path()); let document = sample_document(FarmConfigScope::Workspace); let expected_path = PathBuf::from(dir.path()) - .join("workspace/infra/local/runtime/radroots/config/apps/cli/farm.toml"); + .join("infra/local/runtime/radroots/config/apps/cli/farm.toml"); let written_path = write(&paths, FarmConfigScope::Workspace, &document).expect("write workspace config"); diff --git a/src/runtime/paths.rs b/src/runtime/paths.rs @@ -10,7 +10,6 @@ use crate::runtime::{ config::{EnvFileValues, Environment}, }; -const DEFAULT_WORKSPACE_CONFIG_PATH: &str = "infra/local/runtime/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"; @@ -34,7 +33,7 @@ pub struct PathsConfig { pub shared_accounts_namespace: String, pub shared_identities_namespace: String, pub app_config_path: PathBuf, - pub workspace_config_path: PathBuf, + pub workspace_config_path: Option<PathBuf>, pub app_data_root: PathBuf, pub app_logs_root: PathBuf, pub shared_accounts_data_root: PathBuf, @@ -51,8 +50,7 @@ pub(crate) fn resolve_paths( let (profile, profile_label, profile_source) = resolve_cli_path_profile(env, env_file)?; let override_selection = resolve_cli_path_overrides(current_dir.as_path(), env, env_file, profile)?; - let workspace_config_path = - resolve_workspace_config_path(current_dir.as_path(), profile, &override_selection)?; + let workspace_config_path = resolve_workspace_config_path(profile, &override_selection)?; let resolved = resolver .resolve(profile, &override_selection.overrides) .map_err(|err| RuntimeError::Config(format!("resolve Radroots path roots: {err}")))?; @@ -193,16 +191,15 @@ fn normalize_explicit_path_root(current_dir: &Path, value: &str) -> PathBuf { } fn resolve_workspace_config_path( - current_dir: &Path, profile: RadrootsPathProfile, override_selection: &CliPathOverrideSelection, -) -> Result<PathBuf, RuntimeError> { +) -> Result<Option<PathBuf>, RuntimeError> { match profile { - RadrootsPathProfile::InteractiveUser => Ok(current_dir.join(DEFAULT_WORKSPACE_CONFIG_PATH)), + RadrootsPathProfile::InteractiveUser => Ok(None), RadrootsPathProfile::RepoLocal => override_selection .repo_local_root .as_ref() - .map(|root| root.join(DEFAULT_CONFIG_FILE_NAME)) + .map(|root| Some(root.join(DEFAULT_CONFIG_FILE_NAME))) .ok_or_else(|| { RuntimeError::Config(format!( "{ENV_CLI_PATHS_REPO_LOCAL_ROOT} must be resolved when {ENV_CLI_PATHS_PROFILE}=repo_local" diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs @@ -768,9 +768,7 @@ mod tests { shared_accounts_namespace: "shared/accounts".into(), shared_identities_namespace: "shared/identities".into(), app_config_path: PathBuf::from("/tmp/config/apps/cli/config.toml"), - workspace_config_path: PathBuf::from( - "/tmp/workspace/infra/local/runtime/radroots/config.toml", - ), + workspace_config_path: None, app_data_root: PathBuf::from("/tmp/data"), app_logs_root: PathBuf::from("/tmp/logs"), shared_accounts_data_root: PathBuf::from("/tmp/shared/accounts"), diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs @@ -104,7 +104,6 @@ fn binding_by_capability<'a>(json: &'a Value, capability_id: &str) -> &'a Value #[test] fn config_show_json_reports_default_bootstrap_state() { let dir = tempdir().expect("tempdir"); - let canonical_root = dir.path().canonicalize().expect("canonical tempdir"); let output = runtime_show_command_in(dir.path()) .args(["--json", "config", "show"]) .output() @@ -147,13 +146,8 @@ fn config_show_json_reports_default_bootstrap_state() { .display() .to_string() ); - assert_eq!( - json["paths"]["workspace_config_path"], - canonical_root - .join("infra/local/runtime/radroots/config.toml") - .display() - .to_string() - ); + assert_eq!(json["paths"]["workspace_config_enabled"], false); + assert_eq!(json["paths"]["workspace_config_path"], Value::Null); assert_eq!( json["paths"]["app_data_root"], data_root(dir.path()).join("apps/cli").display().to_string() @@ -471,6 +465,7 @@ fn config_show_json_reports_repo_local_paths_when_requested() { json["paths"]["workspace_config_path"], repo_local_root.join("config.toml").display().to_string() ); + assert_eq!(json["paths"]["workspace_config_enabled"], true); assert_eq!( json["paths"]["app_data_root"], repo_local_root.join("data/apps/cli").display().to_string() @@ -526,6 +521,7 @@ fn config_show_json_reads_repo_local_workspace_config_from_explicit_root() { json["paths"]["workspace_config_path"], repo_local_root.join("config.toml").display().to_string() ); + assert_eq!(json["paths"]["workspace_config_enabled"], true); assert_eq!(json["config_files"]["workspace_present"], true); assert_eq!(json["relay"]["count"], 1); assert_eq!(json["relay"]["urls"][0], "wss://relay.repo-local");