commit 7347747acd0e764d98f4fb58aa63dfb51a50a93b
parent 9ef72a6ecac979ff381de92c5b29685124e1f4c4
Author: triesap <tyson@radroots.org>
Date: Thu, 9 Apr 2026 17:18:30 +0000
runtime: report legacy cli paths
Diffstat:
5 files changed, 179 insertions(+), 6 deletions(-)
diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs
@@ -1,7 +1,8 @@
use crate::domain::runtime::{
AccountRuntimeView, AccountSecretRuntimeView, ConfigFilesRuntimeView, ConfigShowView,
- HyfRuntimeView, LocalRuntimeView, LoggingRuntimeView, MycRuntimeView, OutputRuntimeView,
- PathsRuntimeView, RelayRuntimeView, RpcRuntimeView, SignerRuntimeView,
+ HyfRuntimeView, LegacyPathRuntimeView, LocalRuntimeView, LoggingRuntimeView,
+ MigrationRuntimeView, MycRuntimeView, OutputRuntimeView, PathsRuntimeView, RelayRuntimeView,
+ RpcRuntimeView, SignerRuntimeView,
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
@@ -51,6 +52,7 @@ pub fn show(
.to_string(),
default_identity_path: config.paths.default_identity_path.display().to_string(),
},
+ migration: migration_runtime_view(config),
logging: LoggingRuntimeView {
initialized: logging.initialized,
filter: config.logging.filter.clone(),
@@ -112,3 +114,38 @@ pub fn show(
},
})
}
+
+fn migration_runtime_view(config: &RuntimeConfig) -> MigrationRuntimeView {
+ let report = &config.migration.report;
+ let detected_legacy_paths = report
+ .detected_legacy_paths
+ .iter()
+ .map(|path| LegacyPathRuntimeView {
+ id: path.id.clone(),
+ description: path.description.clone(),
+ path: path.path.display().to_string(),
+ destination: path
+ .destination
+ .as_ref()
+ .map(|destination| destination.display().to_string()),
+ import_hint: path.import_hint.clone(),
+ })
+ .collect::<Vec<_>>();
+ let actions = if detected_legacy_paths.is_empty() {
+ Vec::new()
+ } else {
+ vec![
+ "inspect detected_legacy_paths before writing new local state".to_owned(),
+ "perform an explicit export/import or manual copy; startup did not move legacy data"
+ .to_owned(),
+ ]
+ };
+ MigrationRuntimeView {
+ posture: report.posture.to_owned(),
+ state: report.state.to_owned(),
+ silent_startup_relocation: report.silent_startup_relocation,
+ compatibility_window: report.compatibility_window.to_owned(),
+ detected_legacy_paths,
+ actions,
+ }
+}
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -111,6 +111,7 @@ pub struct ConfigShowView {
pub output: OutputRuntimeView,
pub config_files: ConfigFilesRuntimeView,
pub paths: PathsRuntimeView,
+ pub migration: MigrationRuntimeView,
pub logging: LoggingRuntimeView,
pub account: AccountRuntimeView,
pub signer: SignerRuntimeView,
@@ -122,6 +123,26 @@ pub struct ConfigShowView {
}
#[derive(Debug, Clone, Serialize)]
+pub struct MigrationRuntimeView {
+ pub posture: String,
+ pub state: String,
+ pub silent_startup_relocation: bool,
+ pub compatibility_window: String,
+ pub detected_legacy_paths: Vec<LegacyPathRuntimeView>,
+ pub actions: Vec<String>,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct LegacyPathRuntimeView {
+ pub id: String,
+ pub description: String,
+ pub path: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub destination: Option<String>,
+ pub import_hint: String,
+}
+
+#[derive(Debug, Clone, Serialize)]
pub struct OutputRuntimeView {
pub format: String,
pub verbosity: String,
diff --git a/src/render/mod.rs b/src/render/mod.rs
@@ -2146,11 +2146,12 @@ mod tests {
};
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, LocalConfig,
- LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, RelayConfig,
- RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
- SignerConfig, Verbosity,
+ LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig,
+ RelayConfig, RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig,
+ SignerBackend, SignerConfig, Verbosity,
};
use crate::runtime::logging::LoggingState;
+ use radroots_runtime_paths::RadrootsMigrationReport;
use radroots_secret_vault::RadrootsSecretBackend;
#[test]
@@ -2184,6 +2185,9 @@ mod tests {
default_identity_path:
"/home/tester/.radroots/secrets/shared/identities/default.json".into(),
},
+ migration: MigrationConfig {
+ report: RadrootsMigrationReport::empty(),
+ },
logging: LoggingConfig {
filter: "info".to_owned(),
directory: None,
@@ -2328,6 +2332,9 @@ mod tests {
default_identity_path:
"/home/tester/.radroots/secrets/shared/identities/default.json".into(),
},
+ migration: MigrationConfig {
+ report: RadrootsMigrationReport::empty(),
+ },
logging: LoggingConfig {
filter: "info".to_owned(),
directory: None,
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -3,7 +3,10 @@ use std::fs;
use std::path::Path;
use std::path::PathBuf;
-use radroots_runtime_paths::RadrootsPathResolver;
+use radroots_runtime_paths::{
+ RadrootsLegacyPathCandidate, RadrootsMigrationReport, RadrootsPathResolver,
+ inspect_legacy_paths,
+};
use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend};
use serde::Deserialize;
use url::Url;
@@ -234,6 +237,7 @@ pub struct RpcConfig {
pub struct RuntimeConfig {
pub output: OutputConfig,
pub paths: PathsConfig,
+ pub migration: MigrationConfig,
pub logging: LoggingConfig,
pub account: AccountConfig,
pub account_secret_contract: AccountSecretContractConfig,
@@ -246,6 +250,11 @@ pub struct RuntimeConfig {
pub rpc: RpcConfig,
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MigrationConfig {
+ pub report: RadrootsMigrationReport,
+}
+
#[derive(Debug, Default)]
pub(crate) struct EnvFileValues(BTreeMap<String, String>);
@@ -317,6 +326,7 @@ impl RuntimeConfig {
env_file: &EnvFileValues,
) -> 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 app_config = load_cli_config_file(paths.app_config_path.as_path())?;
let account_secret_backend = resolve_account_secret_backend(args, env, env_file)?
@@ -336,6 +346,7 @@ impl RuntimeConfig {
dry_run: args.dry_run,
},
paths: paths.clone(),
+ migration,
logging: LoggingConfig {
filter: args
.log_filter
@@ -451,6 +462,40 @@ impl RuntimeConfig {
}
}
+fn resolve_migration(paths: PathsConfig, env: &dyn Environment) -> MigrationConfig {
+ MigrationConfig {
+ report: inspect_legacy_paths(legacy_path_candidates(&paths, env)),
+ }
+}
+
+fn legacy_path_candidates(
+ paths: &PathsConfig,
+ env: &dyn Environment,
+) -> Vec<RadrootsLegacyPathCandidate> {
+ let Some(home_dir) = env.var("HOME").map(PathBuf::from) else {
+ return Vec::new();
+ };
+ let old_user_config = home_dir.join(".config/radroots/config.toml");
+ let old_user_state_root = home_dir.join(".local/share/radroots");
+
+ vec![
+ RadrootsLegacyPathCandidate::new(
+ "cli_user_config_v0",
+ "legacy cli user config",
+ old_user_config,
+ Some(paths.app_config_path.clone()),
+ "merge this config into the canonical app config path; the cli will not copy it on startup",
+ ),
+ RadrootsLegacyPathCandidate::new(
+ "cli_user_state_root_v0",
+ "legacy cli user state root",
+ old_user_state_root,
+ Some(paths.app_data_root.clone()),
+ "export/import the old local state into the canonical app and shared namespaces; the cli will not move it on startup",
+ ),
+ ]
+}
+
fn load_cli_config_file(path: &Path) -> Result<Option<CliConfigFile>, RuntimeError> {
if !path.exists() {
return Ok(None);
diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs
@@ -168,6 +168,21 @@ fn config_show_json_reports_default_bootstrap_state() {
.display()
.to_string()
);
+ assert_eq!(
+ json["migration"]["posture"],
+ "explicit_operator_import_required"
+ );
+ assert_eq!(json["migration"]["state"], "ready");
+ assert_eq!(json["migration"]["silent_startup_relocation"], false);
+ assert_eq!(
+ json["migration"]["compatibility_window"],
+ "detect_and_report_only"
+ );
+ assert_eq!(
+ json["migration"]["detected_legacy_paths"],
+ Value::Array(vec![])
+ );
+ assert_eq!(json["migration"]["actions"], Value::Array(vec![]));
assert_eq!(json["logging"]["initialized"], true);
assert_eq!(json["logging"]["stdout"], false);
assert_eq!(
@@ -260,6 +275,54 @@ fn config_show_json_reports_default_bootstrap_state() {
}
#[test]
+fn config_show_json_reports_detected_legacy_cli_paths_without_moving_them() {
+ let dir = tempdir().expect("tempdir");
+ let home = dir.path().join("home");
+ let old_config = home.join(".config/radroots/config.toml");
+ let old_state_root = home.join(".local/share/radroots");
+ fs::create_dir_all(old_config.parent().expect("old config parent")).expect("old config dir");
+ fs::create_dir_all(old_state_root.join("accounts")).expect("old state dir");
+ fs::write(&old_config, "[relay]\nurls = []\n").expect("old config");
+ fs::write(old_state_root.join("accounts/store.json"), "{}").expect("old store");
+
+ let output = runtime_show_command_in(dir.path())
+ .args(["--json", "config", "show"])
+ .output()
+ .expect("run config show");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
+ let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
+
+ assert_eq!(json["migration"]["state"], "legacy_state_detected");
+ assert_eq!(json["migration"]["silent_startup_relocation"], false);
+ let detected = json["migration"]["detected_legacy_paths"]
+ .as_array()
+ .expect("detected legacy paths array");
+ assert_eq!(detected.len(), 2);
+ assert_eq!(detected[0]["id"], "cli_user_config_v0");
+ assert_eq!(detected[0]["path"], old_config.display().to_string());
+ assert_eq!(
+ detected[0]["destination"],
+ config_root(dir.path())
+ .join("apps/cli/config.toml")
+ .display()
+ .to_string()
+ );
+ assert_eq!(detected[1]["id"], "cli_user_state_root_v0");
+ assert_eq!(detected[1]["path"], old_state_root.display().to_string());
+ assert!(
+ json["migration"]["actions"]
+ .as_array()
+ .expect("actions")
+ .iter()
+ .any(|action| action
+ .as_str()
+ .is_some_and(|value| value.contains("startup did not move legacy data")))
+ );
+}
+
+#[test]
fn config_show_json_reports_repo_local_paths_when_requested() {
let dir = tempdir().expect("tempdir");
let repo_local_root = dir.path().join(".local/radroots/dev");