commit fc353575600cd8d8469177c8ecda6acba3e1db4a
parent dafb133352af7148447529d852c4dd3cfd196a44
Author: triesap <tyson@radroots.org>
Date: Mon, 6 Apr 2026 21:17:48 +0000
cli: bootstrap runtime shell
Diffstat:
14 files changed, 1657 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
@@ -11,6 +11,7 @@ Thumbs.db
# Local development files
.vscode/
.idea/
+logs/
# Local secrets
*.pem
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,5 +3,822 @@
version = 4
[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "assert_cmd"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a686bbee5efb88a82df0621b236e74d925f470e5445d3220a5648b892ec99c9"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "libc",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "cc"
+version = "1.2.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.184"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "predicates"
+version = "3.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "predicates-core",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
name = "radroots-cli"
version = "0.1.0"
+dependencies = [
+ "assert_cmd",
+ "clap",
+ "radroots-log",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "radroots-log"
+version = "0.1.0-alpha.1"
+dependencies = [
+ "chrono",
+ "thiserror 1.0.69",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termtree"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-appender"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
+dependencies = [
+ "crossbeam-channel",
+ "thiserror 2.0.18",
+ "time",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
@@ -15,3 +15,13 @@ path = "src/main.rs"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
+
+[dependencies]
+clap = { version = "4.5", features = ["derive"] }
+radroots-log = { path = "../lib/crates/log" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+thiserror = "2.0"
+
+[dev-dependencies]
+assert_cmd = "2.0"
diff --git a/src/cli.rs b/src/cli.rs
@@ -0,0 +1,110 @@
+use clap::{ArgAction, Args, Parser, Subcommand};
+use std::path::PathBuf;
+
+#[derive(Debug, Parser, Clone)]
+#[command(name = "radroots")]
+#[command(version)]
+pub struct CliArgs {
+ #[arg(long, global = true, action = ArgAction::SetTrue)]
+ pub json: bool,
+ #[arg(long, global = true)]
+ pub log_filter: Option<String>,
+ #[arg(long, global = true)]
+ pub log_dir: Option<PathBuf>,
+ #[arg(long = "log-stdout", global = true, action = ArgAction::SetTrue)]
+ pub log_stdout: bool,
+ #[arg(long = "no-log-stdout", global = true, action = ArgAction::SetTrue)]
+ pub no_log_stdout: bool,
+ #[arg(long, global = true)]
+ pub identity_path: Option<PathBuf>,
+ #[arg(long = "allow-generate-identity", global = true, action = ArgAction::SetTrue)]
+ pub allow_generate_identity: bool,
+ #[arg(long = "no-allow-generate-identity", global = true, action = ArgAction::SetTrue)]
+ pub no_allow_generate_identity: bool,
+ #[arg(long, global = true)]
+ pub signer_backend: Option<String>,
+ #[arg(long, global = true)]
+ pub myc_executable: Option<PathBuf>,
+ #[command(subcommand)]
+ pub command: Command,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum Command {
+ Runtime(RuntimeArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct RuntimeArgs {
+ #[command(subcommand)]
+ pub command: RuntimeCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum RuntimeCommand {
+ Show,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{CliArgs, Command, RuntimeCommand};
+ use clap::Parser;
+
+ #[test]
+ fn parses_runtime_show_command() {
+ let parsed = CliArgs::parse_from(["radroots", "runtime", "show"]);
+ match parsed.command {
+ Command::Runtime(runtime) => match runtime.command {
+ RuntimeCommand::Show => {}
+ },
+ }
+ }
+
+ #[test]
+ fn parses_global_runtime_flags() {
+ let parsed = CliArgs::parse_from([
+ "radroots",
+ "--json",
+ "--log-filter",
+ "debug,radroots_cli=trace",
+ "--log-dir",
+ "logs",
+ "--log-stdout",
+ "--identity-path",
+ "identity.local.json",
+ "--allow-generate-identity",
+ "--signer-backend",
+ "myc",
+ "--myc-executable",
+ "bin/myc",
+ "runtime",
+ "show",
+ ]);
+ assert!(parsed.json);
+ assert_eq!(
+ parsed.log_filter.as_deref(),
+ Some("debug,radroots_cli=trace")
+ );
+ assert_eq!(
+ parsed.log_dir.as_deref().and_then(|path| path.to_str()),
+ Some("logs")
+ );
+ assert!(parsed.log_stdout);
+ assert_eq!(
+ parsed
+ .identity_path
+ .as_deref()
+ .and_then(|path| path.to_str()),
+ Some("identity.local.json")
+ );
+ assert!(parsed.allow_generate_identity);
+ assert_eq!(parsed.signer_backend.as_deref(), Some("myc"));
+ assert_eq!(
+ parsed
+ .myc_executable
+ .as_deref()
+ .and_then(|path| path.to_str()),
+ Some("bin/myc")
+ );
+ }
+}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
@@ -0,0 +1,19 @@
+pub mod runtime;
+
+use crate::cli::{Command, RuntimeCommand};
+use crate::domain::CommandOutput;
+use crate::runtime::RuntimeError;
+use crate::runtime::config::RuntimeConfig;
+use crate::runtime::logging::LoggingState;
+
+pub fn dispatch(
+ command: &Command,
+ config: &RuntimeConfig,
+ logging: &LoggingState,
+) -> Result<CommandOutput, RuntimeError> {
+ match command {
+ Command::Runtime(runtime) => match runtime.command {
+ RuntimeCommand::Show => Ok(CommandOutput::RuntimeShow(runtime::show(config, logging))),
+ },
+ }
+}
diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs
@@ -0,0 +1,35 @@
+use crate::domain::runtime::{
+ IdentityRuntimeView, LoggingRuntimeView, MycRuntimeView, RuntimeShowView, SignerRuntimeView,
+};
+use crate::runtime::config::RuntimeConfig;
+use crate::runtime::logging::LoggingState;
+
+pub fn show(config: &RuntimeConfig, logging: &LoggingState) -> RuntimeShowView {
+ RuntimeShowView {
+ output_format: config.output_format.as_str().to_owned(),
+ logging: LoggingRuntimeView {
+ initialized: logging.initialized,
+ filter: config.logging.filter.clone(),
+ stdout: config.logging.stdout,
+ directory: config
+ .logging
+ .directory
+ .as_ref()
+ .map(|path| path.display().to_string()),
+ current_file: logging
+ .current_file
+ .as_ref()
+ .map(|path| path.display().to_string()),
+ },
+ identity: IdentityRuntimeView {
+ path: config.identity.path.display().to_string(),
+ allow_generate: config.identity.allow_generate,
+ },
+ signer: SignerRuntimeView {
+ backend: config.signer.backend.as_str().to_owned(),
+ },
+ myc: MycRuntimeView {
+ executable: config.myc.executable.display().to_string(),
+ },
+ }
+}
diff --git a/src/domain/mod.rs b/src/domain/mod.rs
@@ -0,0 +1,3 @@
+pub mod runtime;
+
+pub use runtime::CommandOutput;
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -0,0 +1,40 @@
+use serde::Serialize;
+
+#[derive(Debug, Clone)]
+pub enum CommandOutput {
+ RuntimeShow(RuntimeShowView),
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct RuntimeShowView {
+ pub output_format: String,
+ pub logging: LoggingRuntimeView,
+ pub identity: IdentityRuntimeView,
+ pub signer: SignerRuntimeView,
+ pub myc: MycRuntimeView,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct LoggingRuntimeView {
+ pub initialized: bool,
+ pub filter: String,
+ pub stdout: bool,
+ pub directory: Option<String>,
+ pub current_file: Option<String>,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct IdentityRuntimeView {
+ pub path: String,
+ pub allow_generate: bool,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct SignerRuntimeView {
+ pub backend: String,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct MycRuntimeView {
+ pub executable: String,
+}
diff --git a/src/main.rs b/src/main.rs
@@ -1,3 +1,36 @@
#![forbid(unsafe_code)]
-fn main() {}
+mod cli;
+mod commands;
+mod domain;
+mod render;
+mod runtime;
+
+use clap::Parser;
+use std::io::Write;
+use std::process::ExitCode;
+
+use crate::cli::CliArgs;
+use crate::commands::dispatch;
+use crate::render::render_output;
+use crate::runtime::config::RuntimeConfig;
+use crate::runtime::logging::initialize_logging;
+
+fn main() -> ExitCode {
+ match run() {
+ Ok(()) => ExitCode::SUCCESS,
+ Err(error) => {
+ let _ = writeln!(std::io::stderr(), "{error}");
+ error.exit_code()
+ }
+ }
+}
+
+fn run() -> Result<(), runtime::RuntimeError> {
+ let args = CliArgs::parse();
+ let config = RuntimeConfig::from_system(&args)?;
+ let logging = initialize_logging(&config.logging)?;
+ let output = dispatch(&args.command, &config, &logging)?;
+ render_output(&output, config.output_format)?;
+ Ok(())
+}
diff --git a/src/render/mod.rs b/src/render/mod.rs
@@ -0,0 +1,116 @@
+use std::io::{self, Write};
+
+use crate::domain::runtime::CommandOutput;
+use crate::runtime::RuntimeError;
+use crate::runtime::config::OutputFormat;
+
+pub fn render_output(output: &CommandOutput, format: OutputFormat) -> Result<(), RuntimeError> {
+ match format {
+ OutputFormat::Human => render_human(output),
+ OutputFormat::Json => render_json(output),
+ }
+}
+
+fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
+ let mut stdout = io::stdout().lock();
+ match output {
+ CommandOutput::RuntimeShow(view) => {
+ writeln!(stdout, "runtime")?;
+ writeln!(stdout, " output format: {}", view.output_format)?;
+ writeln!(stdout, "logging")?;
+ writeln!(
+ stdout,
+ " initialized: {}",
+ yes_no(view.logging.initialized)
+ )?;
+ writeln!(stdout, " filter: {}", view.logging.filter)?;
+ writeln!(stdout, " stdout: {}", yes_no(view.logging.stdout))?;
+ writeln!(
+ stdout,
+ " directory: {}",
+ view.logging.directory.as_deref().unwrap_or("<disabled>")
+ )?;
+ writeln!(
+ stdout,
+ " current file: {}",
+ view.logging.current_file.as_deref().unwrap_or("<disabled>")
+ )?;
+ writeln!(stdout, "identity")?;
+ writeln!(stdout, " path: {}", view.identity.path)?;
+ writeln!(
+ stdout,
+ " allow generate: {}",
+ yes_no(view.identity.allow_generate)
+ )?;
+ writeln!(stdout, "signer")?;
+ writeln!(stdout, " backend: {}", view.signer.backend)?;
+ writeln!(stdout, "myc")?;
+ writeln!(stdout, " executable: {}", view.myc.executable)?;
+ }
+ }
+ Ok(())
+}
+
+fn render_json(output: &CommandOutput) -> Result<(), RuntimeError> {
+ let mut stdout = io::stdout().lock();
+ match output {
+ CommandOutput::RuntimeShow(view) => {
+ serde_json::to_writer_pretty(&mut stdout, view)?;
+ writeln!(stdout)?;
+ }
+ }
+ Ok(())
+}
+
+fn yes_no(value: bool) -> &'static str {
+ if value { "yes" } else { "no" }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::commands::runtime;
+ use crate::runtime::config::{
+ IdentityConfig, LoggingConfig, MycConfig, OutputFormat, RuntimeConfig, SignerBackend,
+ SignerConfig,
+ };
+ use crate::runtime::logging::LoggingState;
+
+ #[test]
+ fn human_render_contains_runtime_sections() {
+ let view = runtime::show(
+ &RuntimeConfig {
+ output_format: OutputFormat::Human,
+ logging: LoggingConfig {
+ filter: "info".to_owned(),
+ directory: None,
+ stdout: false,
+ },
+ identity: IdentityConfig {
+ path: "identity.json".into(),
+ allow_generate: false,
+ },
+ signer: SignerConfig {
+ backend: SignerBackend::Local,
+ },
+ myc: MycConfig {
+ executable: "myc".into(),
+ },
+ },
+ &LoggingState {
+ initialized: true,
+ current_file: None,
+ },
+ );
+ let rendered = format!(
+ "runtime\n output format: {}\nlogging\n initialized: {}\n",
+ view.output_format,
+ if view.logging.initialized {
+ "yes"
+ } else {
+ "no"
+ }
+ );
+ assert!(rendered.contains("runtime"));
+ assert!(rendered.contains("logging"));
+ }
+}
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -0,0 +1,343 @@
+use std::path::PathBuf;
+
+use crate::cli::CliArgs;
+use crate::runtime::RuntimeError;
+
+const DEFAULT_LOG_FILTER: &str = "info";
+const ENV_OUTPUT: &str = "RADROOTS_OUTPUT";
+const ENV_LOG_FILTER: &str = "RADROOTS_LOG_FILTER";
+const ENV_LOG_DIR: &str = "RADROOTS_LOG_DIR";
+const ENV_LOG_STDOUT: &str = "RADROOTS_LOG_STDOUT";
+const ENV_IDENTITY_PATH: &str = "RADROOTS_IDENTITY_PATH";
+const ENV_IDENTITY_ALLOW_GENERATE: &str = "RADROOTS_IDENTITY_ALLOW_GENERATE";
+const ENV_SIGNER_BACKEND: &str = "RADROOTS_SIGNER_BACKEND";
+const ENV_MYC_EXECUTABLE: &str = "RADROOTS_MYC_EXECUTABLE";
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum OutputFormat {
+ Human,
+ Json,
+}
+
+impl OutputFormat {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Human => "human",
+ Self::Json => "json",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct LoggingConfig {
+ pub filter: String,
+ pub directory: Option<PathBuf>,
+ pub stdout: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct IdentityConfig {
+ pub path: PathBuf,
+ pub allow_generate: bool,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SignerBackend {
+ Local,
+ Myc,
+}
+
+impl SignerBackend {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Local => "local",
+ Self::Myc => "myc",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SignerConfig {
+ pub backend: SignerBackend,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MycConfig {
+ pub executable: PathBuf,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RuntimeConfig {
+ pub output_format: OutputFormat,
+ pub logging: LoggingConfig,
+ pub identity: IdentityConfig,
+ pub signer: SignerConfig,
+ pub myc: MycConfig,
+}
+
+pub trait Environment {
+ fn var(&self, key: &str) -> Option<String>;
+}
+
+pub struct SystemEnvironment;
+
+impl Environment for SystemEnvironment {
+ fn var(&self, key: &str) -> Option<String> {
+ std::env::var(key).ok()
+ }
+}
+
+impl RuntimeConfig {
+ pub fn from_system(args: &CliArgs) -> Result<Self, RuntimeError> {
+ Self::resolve(args, &SystemEnvironment)
+ }
+
+ pub fn resolve(args: &CliArgs, env: &dyn Environment) -> Result<Self, RuntimeError> {
+ Ok(Self {
+ output_format: resolve_output_format(args, env)?,
+ logging: LoggingConfig {
+ filter: args
+ .log_filter
+ .clone()
+ .or_else(|| env.var(ENV_LOG_FILTER))
+ .unwrap_or_else(|| DEFAULT_LOG_FILTER.to_owned()),
+ directory: args
+ .log_dir
+ .clone()
+ .or_else(|| env.var(ENV_LOG_DIR).map(PathBuf::from)),
+ stdout: resolve_bool_pair(
+ args.log_stdout,
+ args.no_log_stdout,
+ ENV_LOG_STDOUT,
+ false,
+ env,
+ "--log-stdout",
+ "--no-log-stdout",
+ )?,
+ },
+ identity: IdentityConfig {
+ path: args
+ .identity_path
+ .clone()
+ .or_else(|| env.var(ENV_IDENTITY_PATH).map(PathBuf::from))
+ .unwrap_or_else(|| PathBuf::from("identity.json")),
+ allow_generate: resolve_bool_pair(
+ args.allow_generate_identity,
+ args.no_allow_generate_identity,
+ ENV_IDENTITY_ALLOW_GENERATE,
+ false,
+ env,
+ "--allow-generate-identity",
+ "--no-allow-generate-identity",
+ )?,
+ },
+ signer: SignerConfig {
+ backend: args
+ .signer_backend
+ .clone()
+ .or_else(|| env.var(ENV_SIGNER_BACKEND))
+ .map(parse_signer_backend)
+ .transpose()?
+ .unwrap_or(SignerBackend::Local),
+ },
+ myc: MycConfig {
+ executable: args
+ .myc_executable
+ .clone()
+ .or_else(|| env.var(ENV_MYC_EXECUTABLE).map(PathBuf::from))
+ .unwrap_or_else(|| PathBuf::from("myc")),
+ },
+ })
+ }
+}
+
+fn resolve_output_format(
+ args: &CliArgs,
+ env: &dyn Environment,
+) -> Result<OutputFormat, RuntimeError> {
+ if args.json {
+ return Ok(OutputFormat::Json);
+ }
+ match env.var(ENV_OUTPUT) {
+ Some(value) => parse_output_format(value.as_str()),
+ None => Ok(OutputFormat::Human),
+ }
+}
+
+fn resolve_bool_pair(
+ positive_flag: bool,
+ negative_flag: bool,
+ env_key: &str,
+ default: bool,
+ env: &dyn Environment,
+ positive_label: &str,
+ negative_label: &str,
+) -> Result<bool, RuntimeError> {
+ match (positive_flag, negative_flag) {
+ (true, true) => Err(RuntimeError::Config(format!(
+ "flags {positive_label} and {negative_label} cannot be used together"
+ ))),
+ (true, false) => Ok(true),
+ (false, true) => Ok(false),
+ (false, false) => match env.var(env_key) {
+ Some(value) => parse_bool_env(env_key, value.as_str()),
+ None => Ok(default),
+ },
+ }
+}
+
+fn parse_output_format(value: &str) -> Result<OutputFormat, RuntimeError> {
+ match value.trim().to_ascii_lowercase().as_str() {
+ "human" => Ok(OutputFormat::Human),
+ "json" => Ok(OutputFormat::Json),
+ other => Err(RuntimeError::Config(format!(
+ "{ENV_OUTPUT} must be `human` or `json`, got `{other}`"
+ ))),
+ }
+}
+
+fn parse_signer_backend(value: String) -> Result<SignerBackend, RuntimeError> {
+ match value.trim().to_ascii_lowercase().as_str() {
+ "local" => Ok(SignerBackend::Local),
+ "myc" => Ok(SignerBackend::Myc),
+ other => Err(RuntimeError::Config(format!(
+ "{ENV_SIGNER_BACKEND} or --signer-backend must be `local` or `myc`, got `{other}`"
+ ))),
+ }
+}
+
+fn parse_bool_env(key: &str, value: &str) -> Result<bool, RuntimeError> {
+ match value.trim().to_ascii_lowercase().as_str() {
+ "1" | "true" | "yes" | "on" => Ok(true),
+ "0" | "false" | "no" | "off" => Ok(false),
+ other => Err(RuntimeError::Config(format!(
+ "{key} must be a boolean value, got `{other}`"
+ ))),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{Environment, OutputFormat, RuntimeConfig, SignerBackend};
+ use crate::cli::CliArgs;
+ use clap::Parser;
+ use std::collections::BTreeMap;
+ use std::path::PathBuf;
+
+ struct MapEnvironment(BTreeMap<String, String>);
+
+ impl Environment for MapEnvironment {
+ fn var(&self, key: &str) -> Option<String> {
+ self.0.get(key).cloned()
+ }
+ }
+
+ #[test]
+ fn flags_override_environment_values() {
+ let args = CliArgs::parse_from([
+ "radroots",
+ "--json",
+ "--log-filter",
+ "debug",
+ "--log-stdout",
+ "--identity-path",
+ "custom-identity.json",
+ "--allow-generate-identity",
+ "--signer-backend",
+ "local",
+ "--myc-executable",
+ "bin/myc-cli",
+ "runtime",
+ "show",
+ ]);
+ let env = MapEnvironment(BTreeMap::from([
+ ("RADROOTS_OUTPUT".to_owned(), "human".to_owned()),
+ ("RADROOTS_LOG_FILTER".to_owned(), "trace".to_owned()),
+ ("RADROOTS_LOG_STDOUT".to_owned(), "false".to_owned()),
+ (
+ "RADROOTS_IDENTITY_PATH".to_owned(),
+ "env-identity.json".to_owned(),
+ ),
+ (
+ "RADROOTS_IDENTITY_ALLOW_GENERATE".to_owned(),
+ "false".to_owned(),
+ ),
+ ("RADROOTS_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
+ ("RADROOTS_MYC_EXECUTABLE".to_owned(), "env-myc".to_owned()),
+ ]));
+
+ let resolved = RuntimeConfig::resolve(&args, &env).expect("resolve runtime config");
+ assert_eq!(resolved.output_format, OutputFormat::Json);
+ assert_eq!(resolved.logging.filter, "debug");
+ assert!(resolved.logging.stdout);
+ assert_eq!(
+ resolved.identity.path,
+ PathBuf::from("custom-identity.json")
+ );
+ assert!(resolved.identity.allow_generate);
+ assert_eq!(resolved.signer.backend, SignerBackend::Local);
+ assert_eq!(resolved.myc.executable, PathBuf::from("bin/myc-cli"));
+ }
+
+ #[test]
+ fn environment_values_fill_missing_flags() {
+ let args = CliArgs::parse_from(["radroots", "runtime", "show"]);
+ let env = MapEnvironment(BTreeMap::from([
+ ("RADROOTS_OUTPUT".to_owned(), "json".to_owned()),
+ (
+ "RADROOTS_LOG_FILTER".to_owned(),
+ "debug,cli=trace".to_owned(),
+ ),
+ ("RADROOTS_LOG_DIR".to_owned(), "logs/runtime".to_owned()),
+ ("RADROOTS_LOG_STDOUT".to_owned(), "true".to_owned()),
+ (
+ "RADROOTS_IDENTITY_PATH".to_owned(),
+ "state/identity.json".to_owned(),
+ ),
+ (
+ "RADROOTS_IDENTITY_ALLOW_GENERATE".to_owned(),
+ "true".to_owned(),
+ ),
+ ("RADROOTS_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
+ ("RADROOTS_MYC_EXECUTABLE".to_owned(), "bin/myc".to_owned()),
+ ]));
+
+ let resolved = RuntimeConfig::resolve(&args, &env).expect("resolve runtime config");
+ assert_eq!(resolved.output_format, OutputFormat::Json);
+ assert_eq!(resolved.logging.filter, "debug,cli=trace");
+ assert_eq!(
+ resolved.logging.directory,
+ Some(PathBuf::from("logs/runtime"))
+ );
+ assert!(resolved.logging.stdout);
+ assert_eq!(resolved.identity.path, PathBuf::from("state/identity.json"));
+ assert!(resolved.identity.allow_generate);
+ assert_eq!(resolved.signer.backend, SignerBackend::Myc);
+ assert_eq!(resolved.myc.executable, PathBuf::from("bin/myc"));
+ }
+
+ #[test]
+ fn conflicting_boolean_flags_fail() {
+ let args = CliArgs::parse_from([
+ "radroots",
+ "--log-stdout",
+ "--no-log-stdout",
+ "runtime",
+ "show",
+ ]);
+ let env = MapEnvironment(BTreeMap::new());
+ let error = RuntimeConfig::resolve(&args, &env).expect_err("conflicting flags");
+ assert!(error.to_string().contains("cannot be used together"));
+ }
+
+ #[test]
+ fn invalid_environment_value_fails() {
+ let args = CliArgs::parse_from(["radroots", "runtime", "show"]);
+ let env = MapEnvironment(BTreeMap::from([(
+ "RADROOTS_LOG_STDOUT".to_owned(),
+ "maybe".to_owned(),
+ )]));
+ let error = RuntimeConfig::resolve(&args, &env).expect_err("invalid bool");
+ assert!(error.to_string().contains("RADROOTS_LOG_STDOUT"));
+ }
+}
diff --git a/src/runtime/logging.rs b/src/runtime/logging.rs
@@ -0,0 +1,53 @@
+use std::path::PathBuf;
+
+use radroots_log::{LogFileLayout, LoggingOptions};
+
+use crate::runtime::config::LoggingConfig;
+
+const CLI_LOG_FILE_NAME: &str = "radroots-cli.log";
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct LoggingState {
+ pub initialized: bool,
+ pub current_file: Option<PathBuf>,
+}
+
+pub fn initialize_logging(config: &LoggingConfig) -> Result<LoggingState, radroots_log::Error> {
+ let options = to_radroots_logging_options(config);
+ let state = LoggingState {
+ initialized: true,
+ current_file: options.resolved_current_log_file_path(),
+ };
+ radroots_log::init_logging(options)?;
+ Ok(state)
+}
+
+pub fn to_radroots_logging_options(config: &LoggingConfig) -> LoggingOptions {
+ LoggingOptions {
+ dir: config.directory.clone(),
+ file_name: CLI_LOG_FILE_NAME.to_owned(),
+ stdout: config.stdout,
+ default_level: Some(config.filter.clone()),
+ file_layout: LogFileLayout::PrefixedDate,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::to_radroots_logging_options;
+ use crate::runtime::config::LoggingConfig;
+ use std::path::PathBuf;
+
+ #[test]
+ fn logging_options_use_cli_file_name() {
+ let options = to_radroots_logging_options(&LoggingConfig {
+ filter: "info".to_owned(),
+ directory: Some(PathBuf::from("logs")),
+ stdout: false,
+ });
+ assert_eq!(options.file_name, "radroots-cli.log");
+ assert_eq!(options.default_level.as_deref(), Some("info"));
+ assert_eq!(options.dir, Some(PathBuf::from("logs")));
+ assert!(!options.stdout);
+ }
+}
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
@@ -0,0 +1,25 @@
+pub mod config;
+pub mod logging;
+
+use std::process::ExitCode;
+
+#[derive(Debug, thiserror::Error)]
+pub enum RuntimeError {
+ #[error("{0}")]
+ Config(String),
+ #[error("failed to initialize logging: {0}")]
+ Logging(#[from] radroots_log::Error),
+ #[error("failed to serialize json output: {0}")]
+ Json(#[from] serde_json::Error),
+ #[error("failed to write output: {0}")]
+ Io(#[from] std::io::Error),
+}
+
+impl RuntimeError {
+ pub fn exit_code(&self) -> ExitCode {
+ match self {
+ Self::Config(_) => ExitCode::from(2),
+ Self::Logging(_) | Self::Json(_) | Self::Io(_) => ExitCode::from(1),
+ }
+ }
+}
diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs
@@ -0,0 +1,51 @@
+use std::process::Command;
+
+use assert_cmd::prelude::*;
+use serde_json::Value;
+
+#[test]
+fn runtime_show_json_reports_default_bootstrap_state() {
+ let output = Command::cargo_bin("radroots")
+ .expect("binary")
+ .args(["--json", "runtime", "show"])
+ .output()
+ .expect("run runtime 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["output_format"], "json");
+ assert_eq!(json["logging"]["initialized"], true);
+ assert_eq!(json["logging"]["stdout"], false);
+ assert_eq!(json["logging"]["directory"], Value::Null);
+ assert_eq!(json["identity"]["path"], "identity.json");
+ assert_eq!(json["signer"]["backend"], "local");
+ assert_eq!(json["myc"]["executable"], "myc");
+}
+
+#[test]
+fn runtime_show_json_reflects_environment_configuration() {
+ let output = Command::cargo_bin("radroots")
+ .expect("binary")
+ .env("RADROOTS_OUTPUT", "json")
+ .env("RADROOTS_LOG_FILTER", "debug")
+ .env("RADROOTS_LOG_DIR", "logs/runtime")
+ .env("RADROOTS_LOG_STDOUT", "false")
+ .env("RADROOTS_IDENTITY_PATH", "state/identity.json")
+ .env("RADROOTS_IDENTITY_ALLOW_GENERATE", "true")
+ .env("RADROOTS_SIGNER_BACKEND", "myc")
+ .env("RADROOTS_MYC_EXECUTABLE", "bin/myc")
+ .args(["runtime", "show"])
+ .output()
+ .expect("run runtime 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["logging"]["filter"], "debug");
+ assert_eq!(json["logging"]["directory"], "logs/runtime");
+ assert_eq!(json["identity"]["path"], "state/identity.json");
+ assert_eq!(json["identity"]["allow_generate"], true);
+ assert_eq!(json["signer"]["backend"], "myc");
+ assert_eq!(json["myc"]["executable"], "bin/myc");
+}