cli

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

commit c04bf3c03bb695758c15914040424ff941fa37d8
parent 6b1cc67dd752708e03a9ad810562ea6cdf2c092b
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Apr 2026 01:42:57 +0000

cli: default to local env config

Diffstat:
M.env.example | 2+-
Msrc/runtime/config.rs | 5+++++
Mtests/identity_commands.rs | 34++++++++++++++++++++++++++--------
Mtests/myc_status.rs | 43+++++++++++++++++++++++++++++--------------
Mtests/runtime_show.rs | 24+++++++++++-------------
Mtests/signer_status.rs | 34++++++++++++++++++++++++++--------
6 files changed, 98 insertions(+), 44 deletions(-)

diff --git a/.env.example b/.env.example @@ -1,4 +1,4 @@ -# copy to .env for local cli development +# copy to .env for local cli development; the cli loads .env by default from the repo root RADROOTS_CLI_LOGGING_FILTER=info RADROOTS_CLI_LOGGING_OUTPUT_DIR=/absolute/path/to/radroots-platform-v1/logs/services/local/radroots-cli RADROOTS_CLI_LOGGING_STDOUT=false diff --git a/src/runtime/config.rs b/src/runtime/config.rs @@ -7,6 +7,7 @@ use crate::cli::CliArgs; use crate::runtime::RuntimeError; const DEFAULT_LOG_FILTER: &str = "info"; +const DEFAULT_ENV_PATH: &str = ".env"; const ENV_FILE_PATH: &str = "RADROOTS_ENV_FILE"; const ENV_OUTPUT: &str = "RADROOTS_OUTPUT"; const ENV_CLI_LOG_FILTER: &str = "RADROOTS_CLI_LOGGING_FILTER"; @@ -161,6 +162,10 @@ fn resolve_env_file_path(args: &CliArgs, env: &dyn Environment) -> Option<PathBu args.env_file .clone() .or_else(|| env.var(ENV_FILE_PATH).map(PathBuf::from)) + .or_else(|| { + let default_path = PathBuf::from(DEFAULT_ENV_PATH); + default_path.exists().then_some(default_path) + }) } fn resolve_output_format( diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs @@ -1,16 +1,37 @@ +use std::path::Path; use std::process::Command; use assert_cmd::prelude::*; use serde_json::Value; use tempfile::tempdir; +fn cli_command_in(workdir: &Path) -> Command { + let mut command = Command::cargo_bin("radroots").expect("binary"); + command.current_dir(workdir); + for key in [ + "RADROOTS_ENV_FILE", + "RADROOTS_OUTPUT", + "RADROOTS_CLI_LOGGING_FILTER", + "RADROOTS_CLI_LOGGING_OUTPUT_DIR", + "RADROOTS_CLI_LOGGING_STDOUT", + "RADROOTS_LOG_FILTER", + "RADROOTS_LOG_DIR", + "RADROOTS_LOG_STDOUT", + "RADROOTS_IDENTITY_PATH", + "RADROOTS_SIGNER_BACKEND", + "RADROOTS_MYC_EXECUTABLE", + ] { + command.env_remove(key); + } + command +} + #[test] fn identity_init_json_creates_identity_file() { let dir = tempdir().expect("tempdir"); let identity_path = dir.path().join("identity.json"); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--identity-path", @@ -39,8 +60,7 @@ fn identity_show_json_reads_existing_public_identity() { let dir = tempdir().expect("tempdir"); let identity_path = dir.path().join("identity.json"); - let init = Command::cargo_bin("radroots") - .expect("binary") + let init = cli_command_in(dir.path()) .args([ "--json", "--identity-path", @@ -52,8 +72,7 @@ fn identity_show_json_reads_existing_public_identity() { .expect("run identity init"); assert!(init.status.success()); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--identity-path", @@ -80,8 +99,7 @@ fn identity_show_json_reports_unconfigured_without_creating_identity() { let dir = tempdir().expect("tempdir"); let identity_path = dir.path().join("missing-identity.json"); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--identity-path", diff --git a/tests/myc_status.rs b/tests/myc_status.rs @@ -1,5 +1,6 @@ use std::fs; use std::os::unix::fs::PermissionsExt; +use std::path::Path; use std::process::Command; use std::sync::{Mutex, MutexGuard, OnceLock}; @@ -8,6 +9,27 @@ use radroots_identity::RadrootsIdentity; use serde_json::{Value, json}; use tempfile::tempdir; +fn cli_command_in(workdir: &Path) -> Command { + let mut command = Command::cargo_bin("radroots").expect("binary"); + command.current_dir(workdir); + for key in [ + "RADROOTS_ENV_FILE", + "RADROOTS_OUTPUT", + "RADROOTS_CLI_LOGGING_FILTER", + "RADROOTS_CLI_LOGGING_OUTPUT_DIR", + "RADROOTS_CLI_LOGGING_STDOUT", + "RADROOTS_LOG_FILTER", + "RADROOTS_LOG_DIR", + "RADROOTS_LOG_STDOUT", + "RADROOTS_IDENTITY_PATH", + "RADROOTS_SIGNER_BACKEND", + "RADROOTS_MYC_EXECUTABLE", + ] { + command.env_remove(key); + } + command +} + #[test] fn myc_status_reports_ready_for_valid_full_status_payload() { let _guard = myc_test_guard(); @@ -17,8 +39,7 @@ fn myc_status_reports_ready_for_valid_full_status_payload() { successful_status_script(sample_status_payload(true).to_string()).as_str(), ); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--myc-executable", @@ -49,8 +70,7 @@ fn myc_status_reports_unavailable_for_invalid_status_payload() { let dir = tempdir().expect("tempdir"); let executable = write_fake_myc(dir.path(), "#!/bin/sh\nprintf '%s\\n' 'this is not json'\n"); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--myc-executable", @@ -82,8 +102,7 @@ fn myc_status_reports_degraded_service_as_external_unavailable() { successful_status_script(sample_status_payload(false).to_string()).as_str(), ); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--myc-executable", @@ -116,8 +135,7 @@ fn signer_status_reports_degraded_myc_backend_as_external_unavailable() { successful_status_script(sample_status_payload(false).to_string()).as_str(), ); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--signer-backend", @@ -150,8 +168,7 @@ fn myc_status_reports_unavailable_when_executable_is_missing() { let dir = tempdir().expect("tempdir"); let missing = dir.path().join("missing-myc"); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--myc-executable", @@ -182,8 +199,7 @@ fn myc_status_reports_unavailable_for_non_zero_exit() { "#!/bin/sh\nprintf '%s\\n' 'transport unavailable' >&2\nexit 42\n", ); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--myc-executable", @@ -211,8 +227,7 @@ fn myc_status_reports_unavailable_for_timeout() { "#!/bin/sh\nif [ \"$1\" != \"status\" ] || [ \"$2\" != \"--view\" ] || [ \"$3\" != \"full\" ]; then\n echo \"unexpected args: $*\" >&2\n exit 64\nfi\nexec sleep 5\n", ); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--myc-executable", diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs @@ -1,12 +1,14 @@ use std::fs; +use std::path::Path; use std::process::Command; use assert_cmd::prelude::*; use serde_json::Value; use tempfile::tempdir; -fn runtime_show_command() -> Command { +fn runtime_show_command_in(workdir: &Path) -> Command { let mut command = Command::cargo_bin("radroots").expect("binary"); + command.current_dir(workdir); for key in [ "RADROOTS_ENV_FILE", "RADROOTS_OUTPUT", @@ -27,7 +29,8 @@ fn runtime_show_command() -> Command { #[test] fn runtime_show_json_reports_default_bootstrap_state() { - let output = runtime_show_command() + let dir = tempdir().expect("tempdir"); + let output = runtime_show_command_in(dir.path()) .args(["--json", "runtime", "show"]) .output() .expect("run runtime show"); @@ -46,7 +49,8 @@ fn runtime_show_json_reports_default_bootstrap_state() { #[test] fn runtime_show_json_reflects_environment_configuration() { - let output = runtime_show_command() + let dir = tempdir().expect("tempdir"); + let output = runtime_show_command_in(dir.path()) .env("RADROOTS_OUTPUT", "json") .env("RADROOTS_LOG_FILTER", "debug") .env("RADROOTS_LOG_DIR", "logs/runtime") @@ -69,9 +73,9 @@ fn runtime_show_json_reflects_environment_configuration() { } #[test] -fn runtime_show_json_reads_logging_from_env_file() { +fn runtime_show_json_reads_logging_from_default_env_file() { let temp = tempdir().expect("tempdir"); - let env_path = temp.path().join("radroots-cli.env"); + let env_path = temp.path().join(".env"); let logs_dir = temp.path().join("logs").join("radroots-cli"); fs::write( &env_path, @@ -82,14 +86,8 @@ fn runtime_show_json_reads_logging_from_env_file() { ) .expect("write env file"); - let output = runtime_show_command() - .args([ - "--json", - "--env-file", - env_path.to_str().expect("env path"), - "runtime", - "show", - ]) + let output = runtime_show_command_in(temp.path()) + .args(["--json", "runtime", "show"]) .output() .expect("run runtime show"); diff --git a/tests/signer_status.rs b/tests/signer_status.rs @@ -1,17 +1,38 @@ use std::fs; +use std::path::Path; use std::process::Command; use assert_cmd::prelude::*; use serde_json::Value; use tempfile::tempdir; +fn cli_command_in(workdir: &Path) -> Command { + let mut command = Command::cargo_bin("radroots").expect("binary"); + command.current_dir(workdir); + for key in [ + "RADROOTS_ENV_FILE", + "RADROOTS_OUTPUT", + "RADROOTS_CLI_LOGGING_FILTER", + "RADROOTS_CLI_LOGGING_OUTPUT_DIR", + "RADROOTS_CLI_LOGGING_STDOUT", + "RADROOTS_LOG_FILTER", + "RADROOTS_LOG_DIR", + "RADROOTS_LOG_STDOUT", + "RADROOTS_IDENTITY_PATH", + "RADROOTS_SIGNER_BACKEND", + "RADROOTS_MYC_EXECUTABLE", + ] { + command.env_remove(key); + } + command +} + #[test] fn signer_status_reports_local_ready_when_identity_exists() { let dir = tempdir().expect("tempdir"); let identity_path = dir.path().join("identity.json"); - let init = Command::cargo_bin("radroots") - .expect("binary") + let init = cli_command_in(dir.path()) .args([ "--json", "--identity-path", @@ -23,8 +44,7 @@ fn signer_status_reports_local_ready_when_identity_exists() { .expect("run identity init"); assert!(init.status.success()); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--identity-path", @@ -52,8 +72,7 @@ fn signer_status_reports_local_unconfigured_when_identity_is_missing() { let dir = tempdir().expect("tempdir"); let identity_path = dir.path().join("missing-identity.json"); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--identity-path", @@ -85,8 +104,7 @@ fn signer_status_reports_internal_error_for_invalid_identity_file() { let identity_path = dir.path().join("invalid-identity.json"); fs::write(&identity_path, "{ not valid json").expect("write invalid identity"); - let output = Command::cargo_bin("radroots") - .expect("binary") + let output = cli_command_in(dir.path()) .args([ "--json", "--identity-path",