cli

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

commit fc353575600cd8d8469177c8ecda6acba3e1db4a
parent dafb133352af7148447529d852c4dd3cfd196a44
Author: triesap <tyson@radroots.org>
Date:   Mon,  6 Apr 2026 21:17:48 +0000

cli: bootstrap runtime shell

Diffstat:
M.gitignore | 1+
MCargo.lock | 817+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 10++++++++++
Asrc/cli.rs | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/commands/mod.rs | 19+++++++++++++++++++
Asrc/commands/runtime.rs | 35+++++++++++++++++++++++++++++++++++
Asrc/domain/mod.rs | 3+++
Asrc/domain/runtime.rs | 40++++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 35++++++++++++++++++++++++++++++++++-
Asrc/render/mod.rs | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/runtime/config.rs | 343+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/runtime/logging.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/runtime/mod.rs | 25+++++++++++++++++++++++++
Atests/runtime_show.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
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"); +}